gopkg.in/hashicorp/packer.v1@v1.3.2/provisioner/powershell/provisioner.go (about)

     1  // This package implements a provisioner for Packer that executes powershell
     2  // scripts within the remote machine.
     3  package powershell
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"encoding/xml"
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"log"
    13  	"os"
    14  	"sort"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/packer/common"
    19  	"github.com/hashicorp/packer/common/uuid"
    20  	commonhelper "github.com/hashicorp/packer/helper/common"
    21  	"github.com/hashicorp/packer/helper/config"
    22  	"github.com/hashicorp/packer/packer"
    23  	"github.com/hashicorp/packer/template/interpolate"
    24  )
    25  
    26  var retryableSleep = 2 * time.Second
    27  
    28  var psEscape = strings.NewReplacer(
    29  	"$", "`$",
    30  	"\"", "`\"",
    31  	"`", "``",
    32  	"'", "`'",
    33  )
    34  
    35  type Config struct {
    36  	common.PackerConfig `mapstructure:",squash"`
    37  
    38  	// If true, the script contains binary and line endings will not be
    39  	// converted from Windows to Unix-style.
    40  	Binary bool
    41  
    42  	// An inline script to execute. Multiple strings are all executed in the
    43  	// context of a single shell.
    44  	Inline []string
    45  
    46  	// The local path of the powershell script to upload and execute.
    47  	Script string
    48  
    49  	// An array of multiple scripts to run.
    50  	Scripts []string
    51  
    52  	// An array of environment variables that will be injected before your
    53  	// command(s) are executed.
    54  	Vars []string `mapstructure:"environment_vars"`
    55  
    56  	// The remote path where the local powershell script will be uploaded to.
    57  	// This should be set to a writable file that is in a pre-existing
    58  	// directory.
    59  	RemotePath string `mapstructure:"remote_path"`
    60  
    61  	// The remote path where the file containing the environment variables
    62  	// will be uploaded to. This should be set to a writable file that is in a
    63  	// pre-existing directory.
    64  	RemoteEnvVarPath string `mapstructure:"remote_env_var_path"`
    65  
    66  	// The command used to execute the script. The '{{ .Path }}' variable
    67  	// should be used to specify where the script goes, {{ .Vars }} can be
    68  	// used to inject the environment_vars into the environment.
    69  	ExecuteCommand string `mapstructure:"execute_command"`
    70  
    71  	// The command used to execute the elevated script. The '{{ .Path }}'
    72  	// variable should be used to specify where the script goes, {{ .Vars }}
    73  	// can be used to inject the environment_vars into the environment.
    74  	ElevatedExecuteCommand string `mapstructure:"elevated_execute_command"`
    75  
    76  	// The timeout for retrying to start the process. Until this timeout is
    77  	// reached, if the provisioner can't start a process, it retries.  This
    78  	// can be set high to allow for reboots.
    79  	StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"`
    80  
    81  	// This is used in the template generation to format environment variables
    82  	// inside the `ExecuteCommand` template.
    83  	EnvVarFormat string
    84  
    85  	// This is used in the template generation to format environment variables
    86  	// inside the `ElevatedExecuteCommand` template.
    87  	ElevatedEnvVarFormat string `mapstructure:"elevated_env_var_format"`
    88  
    89  	// Instructs the communicator to run the remote script as a Windows
    90  	// scheduled task, effectively elevating the remote user by impersonating
    91  	// a logged-in user
    92  	ElevatedUser     string `mapstructure:"elevated_user"`
    93  	ElevatedPassword string `mapstructure:"elevated_password"`
    94  
    95  	// Valid Exit Codes - 0 is not always the only valid error code!  See
    96  	// http://www.symantec.com/connect/articles/windows-system-error-codes-exit-codes-description
    97  	// for examples such as 3010 - "The requested operation is successful.
    98  	// Changes will not be effective until the system is rebooted."
    99  	ValidExitCodes []int `mapstructure:"valid_exit_codes"`
   100  
   101  	ctx interpolate.Context
   102  }
   103  
   104  type Provisioner struct {
   105  	config       Config
   106  	communicator packer.Communicator
   107  }
   108  
   109  type ExecuteCommandTemplate struct {
   110  	Vars          string
   111  	Path          string
   112  	WinRMPassword string
   113  }
   114  
   115  type EnvVarsTemplate struct {
   116  	WinRMPassword string
   117  }
   118  
   119  func (p *Provisioner) Prepare(raws ...interface{}) error {
   120  	// Create passthrough for winrm password so we can fill it in once we know
   121  	// it
   122  	p.config.ctx.Data = &EnvVarsTemplate{
   123  		WinRMPassword: `{{.WinRMPassword}}`,
   124  	}
   125  
   126  	err := config.Decode(&p.config, &config.DecodeOpts{
   127  		Interpolate:        true,
   128  		InterpolateContext: &p.config.ctx,
   129  		InterpolateFilter: &interpolate.RenderFilter{
   130  			Exclude: []string{
   131  				"execute_command",
   132  				"elevated_execute_command",
   133  			},
   134  		},
   135  	}, raws...)
   136  
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	if p.config.EnvVarFormat == "" {
   142  		p.config.EnvVarFormat = `$env:%s="%s"; `
   143  	}
   144  
   145  	if p.config.ElevatedEnvVarFormat == "" {
   146  		p.config.ElevatedEnvVarFormat = `$env:%s="%s"; `
   147  	}
   148  
   149  	if p.config.ExecuteCommand == "" {
   150  		p.config.ExecuteCommand = `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"`
   151  	}
   152  
   153  	if p.config.ElevatedExecuteCommand == "" {
   154  		p.config.ElevatedExecuteCommand = `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"`
   155  	}
   156  
   157  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
   158  		p.config.Inline = nil
   159  	}
   160  
   161  	if p.config.StartRetryTimeout == 0 {
   162  		p.config.StartRetryTimeout = 5 * time.Minute
   163  	}
   164  
   165  	if p.config.RemotePath == "" {
   166  		uuid := uuid.TimeOrderedUUID()
   167  		p.config.RemotePath = fmt.Sprintf(`c:/Windows/Temp/script-%s.ps1`, uuid)
   168  	}
   169  
   170  	if p.config.RemoteEnvVarPath == "" {
   171  		uuid := uuid.TimeOrderedUUID()
   172  		p.config.RemoteEnvVarPath = fmt.Sprintf(`c:/Windows/Temp/packer-ps-env-vars-%s.ps1`, uuid)
   173  	}
   174  
   175  	if p.config.Scripts == nil {
   176  		p.config.Scripts = make([]string, 0)
   177  	}
   178  
   179  	if p.config.Vars == nil {
   180  		p.config.Vars = make([]string, 0)
   181  	}
   182  
   183  	if p.config.ValidExitCodes == nil {
   184  		p.config.ValidExitCodes = []int{0}
   185  	}
   186  
   187  	var errs error
   188  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   189  		errs = packer.MultiErrorAppend(errs,
   190  			errors.New("Only one of script or scripts can be specified."))
   191  	}
   192  
   193  	if p.config.ElevatedUser != "" && p.config.ElevatedPassword == "" {
   194  		errs = packer.MultiErrorAppend(errs,
   195  			errors.New("Must supply an 'elevated_password' if 'elevated_user' provided"))
   196  	}
   197  
   198  	if p.config.ElevatedUser == "" && p.config.ElevatedPassword != "" {
   199  		errs = packer.MultiErrorAppend(errs,
   200  			errors.New("Must supply an 'elevated_user' if 'elevated_password' provided"))
   201  	}
   202  
   203  	if p.config.Script != "" {
   204  		p.config.Scripts = []string{p.config.Script}
   205  	}
   206  
   207  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   208  		errs = packer.MultiErrorAppend(errs,
   209  			errors.New("Either a script file or inline script must be specified."))
   210  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   211  		errs = packer.MultiErrorAppend(errs,
   212  			errors.New("Only a script file or an inline script can be specified, not both."))
   213  	}
   214  
   215  	for _, path := range p.config.Scripts {
   216  		if _, err := os.Stat(path); err != nil {
   217  			errs = packer.MultiErrorAppend(errs,
   218  				fmt.Errorf("Bad script '%s': %s", path, err))
   219  		}
   220  	}
   221  
   222  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   223  	for _, kv := range p.config.Vars {
   224  		vs := strings.SplitN(kv, "=", 2)
   225  		if len(vs) != 2 || vs[0] == "" {
   226  			errs = packer.MultiErrorAppend(errs,
   227  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   228  		}
   229  	}
   230  
   231  	if errs != nil {
   232  		return errs
   233  	}
   234  
   235  	return nil
   236  }
   237  
   238  // Takes the inline scripts, concatenates them into a temporary file and
   239  // returns a string containing the location of said file.
   240  func extractScript(p *Provisioner) (string, error) {
   241  	temp, err := ioutil.TempFile(os.TempDir(), "packer-powershell-provisioner")
   242  	if err != nil {
   243  		return "", err
   244  	}
   245  	defer temp.Close()
   246  	writer := bufio.NewWriter(temp)
   247  	for _, command := range p.config.Inline {
   248  		log.Printf("Found command: %s", command)
   249  		if _, err := writer.WriteString(command + "\n"); err != nil {
   250  			return "", fmt.Errorf("Error preparing powershell script: %s", err)
   251  		}
   252  	}
   253  
   254  	if err := writer.Flush(); err != nil {
   255  		return "", fmt.Errorf("Error preparing powershell script: %s", err)
   256  	}
   257  
   258  	return temp.Name(), nil
   259  }
   260  
   261  func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
   262  	ui.Say(fmt.Sprintf("Provisioning with Powershell..."))
   263  	p.communicator = comm
   264  
   265  	scripts := make([]string, len(p.config.Scripts))
   266  	copy(scripts, p.config.Scripts)
   267  
   268  	if p.config.Inline != nil {
   269  		temp, err := extractScript(p)
   270  		if err != nil {
   271  			ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err))
   272  		}
   273  		scripts = append(scripts, temp)
   274  		// Remove temp script containing the inline commands when done
   275  		defer os.Remove(temp)
   276  	}
   277  
   278  	for _, path := range scripts {
   279  		ui.Say(fmt.Sprintf("Provisioning with powershell script: %s", path))
   280  
   281  		log.Printf("Opening %s for reading", path)
   282  		f, err := os.Open(path)
   283  		if err != nil {
   284  			return fmt.Errorf("Error opening powershell script: %s", err)
   285  		}
   286  		defer f.Close()
   287  
   288  		command, err := p.createCommandText()
   289  		if err != nil {
   290  			return fmt.Errorf("Error processing command: %s", err)
   291  		}
   292  
   293  		// Upload the file and run the command. Do this in the context of a
   294  		// single retryable function so that we don't end up with the case
   295  		// that the upload succeeded, a restart is initiated, and then the
   296  		// command is executed but the file doesn't exist any longer.
   297  		var cmd *packer.RemoteCmd
   298  		err = p.retryable(func() error {
   299  			if _, err := f.Seek(0, 0); err != nil {
   300  				return err
   301  			}
   302  			if err := comm.Upload(p.config.RemotePath, f, nil); err != nil {
   303  				return fmt.Errorf("Error uploading script: %s", err)
   304  			}
   305  
   306  			cmd = &packer.RemoteCmd{Command: command}
   307  			return cmd.StartWithUi(comm, ui)
   308  		})
   309  		if err != nil {
   310  			return err
   311  		}
   312  
   313  		// Close the original file since we copied it
   314  		f.Close()
   315  
   316  		// Check exit code against allowed codes (likely just 0)
   317  		validExitCode := false
   318  		for _, v := range p.config.ValidExitCodes {
   319  			if cmd.ExitStatus == v {
   320  				validExitCode = true
   321  			}
   322  		}
   323  		if !validExitCode {
   324  			return fmt.Errorf(
   325  				"Script exited with non-zero exit status: %d. Allowed exit codes are: %v",
   326  				cmd.ExitStatus, p.config.ValidExitCodes)
   327  		}
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  func (p *Provisioner) Cancel() {
   334  	// Just hard quit. It isn't a big deal if what we're doing keeps running
   335  	// on the other side.
   336  	os.Exit(0)
   337  }
   338  
   339  // retryable will retry the given function over and over until a non-error is
   340  // returned.
   341  func (p *Provisioner) retryable(f func() error) error {
   342  	startTimeout := time.After(p.config.StartRetryTimeout)
   343  	for {
   344  		var err error
   345  		if err = f(); err == nil {
   346  			return nil
   347  		}
   348  
   349  		// Create an error and log it
   350  		err = fmt.Errorf("Retryable error: %s", err)
   351  		log.Print(err.Error())
   352  
   353  		// Check if we timed out, otherwise we retry. It is safe to retry
   354  		// since the only error case above is if the command failed to START.
   355  		select {
   356  		case <-startTimeout:
   357  			return err
   358  		default:
   359  			time.Sleep(retryableSleep)
   360  		}
   361  	}
   362  }
   363  
   364  // Environment variables required within the remote environment are uploaded
   365  // within a PS script and then enabled by 'dot sourcing' the script
   366  // immediately prior to execution of the main command
   367  func (p *Provisioner) prepareEnvVars(elevated bool) (err error) {
   368  	// Collate all required env vars into a plain string with required
   369  	// formatting applied
   370  	flattenedEnvVars := p.createFlattenedEnvVars(elevated)
   371  	// Create a powershell script on the target build fs containing the
   372  	// flattened env vars
   373  	err = p.uploadEnvVars(flattenedEnvVars)
   374  	if err != nil {
   375  		return err
   376  	}
   377  	return
   378  }
   379  
   380  func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) {
   381  	flattened = ""
   382  	envVars := make(map[string]string)
   383  
   384  	// Always available Packer provided env vars
   385  	envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName
   386  	envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType
   387  
   388  	httpAddr := common.GetHTTPAddr()
   389  	if httpAddr != "" {
   390  		envVars["PACKER_HTTP_ADDR"] = httpAddr
   391  	}
   392  
   393  	// interpolate environment variables
   394  	p.config.ctx.Data = &EnvVarsTemplate{
   395  		WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
   396  	}
   397  	// Split vars into key/value components
   398  	for _, envVar := range p.config.Vars {
   399  		envVar, err := interpolate.Render(envVar, &p.config.ctx)
   400  		if err != nil {
   401  			return
   402  		}
   403  		keyValue := strings.SplitN(envVar, "=", 2)
   404  		// Escape chars special to PS in each env var value
   405  		escapedEnvVarValue := psEscape.Replace(keyValue[1])
   406  		if escapedEnvVarValue != keyValue[1] {
   407  			log.Printf("Env var %s converted to %s after escaping chars special to PS", keyValue[1],
   408  				escapedEnvVarValue)
   409  		}
   410  		envVars[keyValue[0]] = escapedEnvVarValue
   411  	}
   412  
   413  	// Create a list of env var keys in sorted order
   414  	var keys []string
   415  	for k := range envVars {
   416  		keys = append(keys, k)
   417  	}
   418  	sort.Strings(keys)
   419  	format := p.config.EnvVarFormat
   420  	if elevated {
   421  		format = p.config.ElevatedEnvVarFormat
   422  	}
   423  
   424  	// Re-assemble vars using OS specific format pattern and flatten
   425  	for _, key := range keys {
   426  		flattened += fmt.Sprintf(format, key, envVars[key])
   427  	}
   428  	return
   429  }
   430  
   431  func (p *Provisioner) uploadEnvVars(flattenedEnvVars string) (err error) {
   432  	// Upload all env vars to a powershell script on the target build file
   433  	// system. Do this in the context of a single retryable function so that
   434  	// we gracefully handle any errors created by transient conditions such as
   435  	// a system restart
   436  	envVarReader := strings.NewReader(flattenedEnvVars)
   437  	log.Printf("Uploading env vars to %s", p.config.RemoteEnvVarPath)
   438  	err = p.retryable(func() error {
   439  		if err := p.communicator.Upload(p.config.RemoteEnvVarPath, envVarReader, nil); err != nil {
   440  			return fmt.Errorf("Error uploading ps script containing env vars: %s", err)
   441  		}
   442  		return err
   443  	})
   444  	if err != nil {
   445  		return err
   446  	}
   447  	return
   448  }
   449  
   450  func (p *Provisioner) createCommandText() (command string, err error) {
   451  	// Return the interpolated command
   452  	if p.config.ElevatedUser == "" {
   453  		return p.createCommandTextNonPrivileged()
   454  	} else {
   455  		return p.createCommandTextPrivileged()
   456  	}
   457  }
   458  
   459  func (p *Provisioner) createCommandTextNonPrivileged() (command string, err error) {
   460  	// Prepare everything needed to enable the required env vars within the
   461  	// remote environment
   462  	err = p.prepareEnvVars(false)
   463  	if err != nil {
   464  		return "", err
   465  	}
   466  
   467  	p.config.ctx.Data = &ExecuteCommandTemplate{
   468  		Path:          p.config.RemotePath,
   469  		Vars:          p.config.RemoteEnvVarPath,
   470  		WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
   471  	}
   472  	command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   473  
   474  	if err != nil {
   475  		return "", fmt.Errorf("Error processing command: %s", err)
   476  	}
   477  
   478  	// Return the interpolated command
   479  	return command, nil
   480  }
   481  
   482  func getWinRMPassword(buildName string) string {
   483  	winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
   484  	packer.LogSecretFilter.Set(winRMPass)
   485  	return winRMPass
   486  }
   487  
   488  func (p *Provisioner) createCommandTextPrivileged() (command string, err error) {
   489  	// Prepare everything needed to enable the required env vars within the
   490  	// remote environment
   491  	err = p.prepareEnvVars(true)
   492  	if err != nil {
   493  		return "", err
   494  	}
   495  
   496  	p.config.ctx.Data = &ExecuteCommandTemplate{
   497  		Path:          p.config.RemotePath,
   498  		Vars:          p.config.RemoteEnvVarPath,
   499  		WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
   500  	}
   501  	command, err = interpolate.Render(p.config.ElevatedExecuteCommand, &p.config.ctx)
   502  	if err != nil {
   503  		return "", fmt.Errorf("Error processing command: %s", err)
   504  	}
   505  
   506  	// OK so we need an elevated shell runner to wrap our command, this is
   507  	// going to have its own path generate the script and update the command
   508  	// runner in the process
   509  	path, err := p.generateElevatedRunner(command)
   510  	if err != nil {
   511  		return "", fmt.Errorf("Error generating elevated runner: %s", err)
   512  	}
   513  
   514  	// Return the path to the elevated shell wrapper
   515  	command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path)
   516  
   517  	return command, err
   518  }
   519  
   520  func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) {
   521  	log.Printf("Building elevated command wrapper for: %s", command)
   522  
   523  	var buffer bytes.Buffer
   524  
   525  	// Output from the elevated command cannot be returned directly to the
   526  	// Packer console. In order to be able to view output from elevated
   527  	// commands and scripts an indirect approach is used by which the commands
   528  	// output is first redirected to file. The output file is then 'watched'
   529  	// by Packer while the elevated command is running and any content
   530  	// appearing in the file is written out to the console.  Below the portion
   531  	// of command required to redirect output from the command to file is
   532  	// built and appended to the existing command string
   533  	taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
   534  	// Only use %ENVVAR% format for environment variables when setting the log
   535  	// file path; Do NOT use $env:ENVVAR format as it won't be expanded
   536  	// correctly in the elevatedTemplate
   537  	logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out"
   538  	command += fmt.Sprintf(" > %s 2>&1", logFile)
   539  
   540  	// elevatedTemplate wraps the command in a single quoted XML text string
   541  	// so we need to escape characters considered 'special' in XML.
   542  	err = xml.EscapeText(&buffer, []byte(command))
   543  	if err != nil {
   544  		return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err)
   545  	}
   546  	escapedCommand := buffer.String()
   547  	log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand)
   548  	buffer.Reset()
   549  
   550  	// Escape chars special to PowerShell in the ElevatedUser string
   551  	escapedElevatedUser := psEscape.Replace(p.config.ElevatedUser)
   552  	if escapedElevatedUser != p.config.ElevatedUser {
   553  		log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell",
   554  			p.config.ElevatedUser, escapedElevatedUser)
   555  	}
   556  	// Replace ElevatedPassword for winrm users who used this feature
   557  	p.config.ctx.Data = &EnvVarsTemplate{
   558  		WinRMPassword: getWinRMPassword(p.config.PackerBuildName),
   559  	}
   560  
   561  	p.config.ElevatedPassword, _ = interpolate.Render(p.config.ElevatedPassword, &p.config.ctx)
   562  
   563  	// Escape chars special to PowerShell in the ElevatedPassword string
   564  	escapedElevatedPassword := psEscape.Replace(p.config.ElevatedPassword)
   565  	if escapedElevatedPassword != p.config.ElevatedPassword {
   566  		log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell",
   567  			p.config.ElevatedPassword, escapedElevatedPassword)
   568  	}
   569  
   570  	// Generate command
   571  	err = elevatedTemplate.Execute(&buffer, elevatedOptions{
   572  		User:              escapedElevatedUser,
   573  		Password:          escapedElevatedPassword,
   574  		TaskName:          taskName,
   575  		TaskDescription:   "Packer elevated task",
   576  		LogFile:           logFile,
   577  		XMLEscapedCommand: escapedCommand,
   578  	})
   579  
   580  	if err != nil {
   581  		fmt.Printf("Error creating elevated template: %s", err)
   582  		return "", err
   583  	}
   584  	uuid := uuid.TimeOrderedUUID()
   585  	path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid)
   586  	log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path)
   587  	err = p.communicator.Upload(path, &buffer, nil)
   588  	if err != nil {
   589  		return "", fmt.Errorf("Error preparing elevated powershell script: %s", err)
   590  	}
   591  	return path, err
   592  }