diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index c5a4845f7..82ec98c82 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -57,6 +57,11 @@ type Config struct { // This should be set to a writable file that is in a pre-existing directory. RemotePath string `mapstructure:"remote_path"` + // The remote path where the file containing the environment variables + // will be uploaded to. This should be set to a writable file that is + // in a pre-existing directory. + RemoteEnvVarPath string `mapstructure:"remote_env_var_path"` + // The command used to execute the script. The '{{ .Path }}' variable // should be used to specify where the script goes, {{ .Vars }} // can be used to inject the environment_vars into the environment. @@ -159,6 +164,11 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { p.config.RemotePath = fmt.Sprintf(`c:/Windows/Temp/script-%s.ps1`, uuid) } + if p.config.RemoteEnvVarPath == "" { + uuid := uuid.TimeOrderedUUID() + p.config.RemoteEnvVarPath = fmt.Sprintf(`c:/Windows/Temp/packer-ps-env-vars-%s.ps1`, uuid) + } + if p.config.Scripts == nil { p.config.Scripts = make([]string, 0) } @@ -351,13 +361,13 @@ func (p *Provisioner) retryable(f func() error) error { // Environment variables required within the remote environment are uploaded within a PS script and // then enabled by 'dot sourcing' the script immediately prior to execution of the main command -func (p *Provisioner) prepareEnvVars(elevated bool) (envVarPath string, err error) { +func (p *Provisioner) prepareEnvVars(elevated bool) (err error) { // Collate all required env vars into a plain string with required formatting applied flattenedEnvVars := p.createFlattenedEnvVars(elevated) // Create a powershell script on the target build fs containing the flattened env vars - envVarPath, err = p.uploadEnvVars(flattenedEnvVars) + err = p.uploadEnvVars(flattenedEnvVars) if err != nil { - return "", err + return err } return } @@ -413,15 +423,13 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { return } -func (p *Provisioner) uploadEnvVars(flattenedEnvVars string) (envVarPath string, err error) { +func (p *Provisioner) uploadEnvVars(flattenedEnvVars string) (err error) { // Upload all env vars to a powershell script on the target build file system envVarReader := strings.NewReader(flattenedEnvVars) - uuid := uuid.TimeOrderedUUID() - envVarPath = fmt.Sprintf(`${env:SYSTEMROOT}/Temp/packer-env-vars-%s.ps1`, uuid) - log.Printf("Uploading env vars to %s", envVarPath) - err = p.communicator.Upload(envVarPath, envVarReader, nil) + log.Printf("Uploading env vars to %s", p.config.RemoteEnvVarPath) + err = p.communicator.Upload(p.config.RemoteEnvVarPath, envVarReader, nil) if err != nil { - return "", fmt.Errorf("Error uploading ps script containing env vars: %s", err) + return fmt.Errorf("Error uploading ps script containing env vars: %s", err) } return } @@ -437,14 +445,14 @@ func (p *Provisioner) createCommandText() (command string, err error) { func (p *Provisioner) createCommandTextNonPrivileged() (command string, err error) { // Prepare everything needed to enable the required env vars within the remote environment - envVarPath, err := p.prepareEnvVars(false) + err = p.prepareEnvVars(false) if err != nil { return "", err } p.config.ctx.Data = &ExecuteCommandTemplate{ Path: p.config.RemotePath, - Vars: envVarPath, + Vars: p.config.RemoteEnvVarPath, WinRMPassword: getWinRMPassword(p.config.PackerBuildName), } command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) @@ -464,14 +472,14 @@ func getWinRMPassword(buildName string) string { func (p *Provisioner) createCommandTextPrivileged() (command string, err error) { // Prepare everything needed to enable the required env vars within the remote environment - envVarPath, err := p.prepareEnvVars(true) + err = p.prepareEnvVars(true) if err != nil { return "", err } p.config.ctx.Data = &ExecuteCommandTemplate{ Path: p.config.RemotePath, - Vars: envVarPath, + Vars: p.config.RemoteEnvVarPath, WinRMPassword: getWinRMPassword(p.config.PackerBuildName), } command, err = interpolate.Render(p.config.ElevatedExecuteCommand, &p.config.ctx) @@ -557,14 +565,11 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin return "", err } uuid := uuid.TimeOrderedUUID() - path := fmt.Sprintf(`${env:TEMP}/packer-elevated-shell-%s.ps1`, uuid) + path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid) log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path) err = p.communicator.Upload(path, &buffer, nil) if err != nil { return "", fmt.Errorf("Error preparing elevated powershell script: %s", err) } - - // CMD formatted Path required for this op - path = fmt.Sprintf("%s-%s.ps1", "%TEMP%/packer-elevated-shell", uuid) return path, err } diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go index 9c8237eb8..06fe39ef7 100644 --- a/provisioner/powershell/provisioner_test.go +++ b/provisioner/powershell/provisioner_test.go @@ -414,7 +414,7 @@ func TestProvisionerProvision_Inline(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. \${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1';exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1';exit \$LastExitCode }"`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -434,7 +434,7 @@ func TestProvisionerProvision_Inline(t *testing.T) { } cmd = comm.StartCmd.Command - re = regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. \${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1';exit \$LastExitCode }"`) + re = regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1';exit \$LastExitCode }"`) matched = re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -461,7 +461,7 @@ func TestProvisionerProvision_Scripts(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. \${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -495,7 +495,7 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. \${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -612,7 +612,7 @@ func TestProvision_createCommandText(t *testing.T) { // Non-elevated cmd, _ := p.createCommandText() - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. \${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){\$ProgressPreference='SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1';exit \$LastExitCode }"`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -622,7 +622,7 @@ func TestProvision_createCommandText(t *testing.T) { p.config.ElevatedUser = "vagrant" p.config.ElevatedPassword = "vagrant" cmd, _ = p.createCommandText() - re = regexp.MustCompile(`powershell -executionpolicy bypass -file "%TEMP%/packer-elevated-shell-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1"`) + re = regexp.MustCompile(`powershell -executionpolicy bypass -file "C:/Windows/Temp/packer-elevated-shell-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1"`) matched = re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected elevated command: %s", cmd) @@ -636,7 +636,7 @@ func TestProvision_uploadEnvVars(t *testing.T) { flattenedEnvVars := `$env:PACKER_BUILDER_TYPE="footype"; $env:PACKER_BUILD_NAME="foobuild";` - envVarPath, err := p.uploadEnvVars(flattenedEnvVars) + err := p.uploadEnvVars(flattenedEnvVars) if err != nil { t.Fatalf("Did not expect error: %s", err.Error()) } @@ -644,12 +644,6 @@ func TestProvision_uploadEnvVars(t *testing.T) { if comm.UploadCalled != true { t.Fatalf("Failed to upload env var file") } - - re := regexp.MustCompile(`\${env:SYSTEMROOT}/Temp/packer-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1`) - matched := re.MatchString(envVarPath) - if !matched { - t.Fatalf("Got unexpected path for env var file: %s", envVarPath) - } } func TestProvision_generateElevatedShellRunner(t *testing.T) { @@ -670,7 +664,7 @@ func TestProvision_generateElevatedShellRunner(t *testing.T) { t.Fatalf("Should have uploaded file") } - matched, _ := regexp.MatchString("%TEMP%(.{1})packer-elevated-shell.*", path) + matched, _ := regexp.MatchString("C:/Windows/Temp/packer-elevated-shell.*", path) if !matched { t.Fatalf("Got unexpected file: %s", path) } diff --git a/website/source/docs/provisioners/powershell.html.md b/website/source/docs/provisioners/powershell.html.md index 160fe7fab..5f4525aba 100644 --- a/website/source/docs/provisioners/powershell.html.md +++ b/website/source/docs/provisioners/powershell.html.md @@ -13,7 +13,11 @@ sidebar_current: 'docs-provisioners-powershell' Type: `powershell` The PowerShell Packer provisioner runs PowerShell scripts on Windows machines. -It assumes that the communicator in use is WinRM. +It assumes that the communicator in use is WinRM. However, the provisioner +can work equally well (with a few caveats) when combined with the SSH +communicator. See the [section +below](/docs/provisioners/powershell.html#combining-the-powershell-provisioner-with-the-ssh-communicator) +for details. ## Basic Example @@ -97,7 +101,9 @@ Optional parameters: template](/docs/templates/engine.html). There are two available variables: `Path`, which is the path to the script to run, and `Vars`, which is the location of a temp file containing the list of - `environment_vars`, if configured. + `environment_vars`. The value of both `Path` and `Vars` can be + manually configured by setting the values for `remote_path` and + `remote_env_var_path` respectively. - `elevated_user` and `elevated_password` (string) - If specified, the PowerShell script will be run with elevated privileges using the given @@ -111,9 +117,28 @@ Optional parameters: "elevated_password": "{{.WinRMPassword}}", ``` -- `remote_path` (string) - The path where the script will be uploaded to in - the machine. This defaults to "c:/Windows/Temp/script.ps1". This value must - be a writable location and any parent directories must already exist. +- `remote_path` (string) - The path where the PowerShell script will be + uploaded to within the target build machine. This defaults to + `C:/Windows/Temp/script-UUID.ps1` where UUID is replaced with a + dynamically generated string that uniquely identifies the script. + + This setting allows users to override the default upload location. The + value must be a writable location and any parent directories must + already exist. + +- `remote_env_var_path` (string) - Environment variables required within + the remote environment are uploaded within a PowerShell script and then + enabled by 'dot sourcing' the script immediately prior to execution of + the main command or script. + + The path the environment variables script will be uploaded to defaults to + `C:/Windows/Temp/packer-ps-env-vars-UUID.ps1` where UUID is replaced + with a dynamically generated string that uniquely identifies the + script. + + This setting allows users to override the location the environment + variable script is uploaded to. The value must be a writable location + and any parent directories must already exist. - `start_retry_timeout` (string) - The amount of time to attempt to *start* the remote process. By default this is "5m" or 5 minutes. This setting @@ -147,6 +172,41 @@ commonly useful environmental variables: slower speeds using the default file provisioner. A file provisioner using the `winrm` communicator may experience these types of difficulties. +## Combining the PowerShell Provisioner with the SSH Communicator + +The good news first. If you are using the +[Microsoft port of OpenSSH](https://github.com/PowerShell/Win32-OpenSSH/wiki) +then the provisioner should just work as expected - no extra configuration +effort is required. + +Now the caveats. If you are using an alternative configuration, and your SSH +connection lands you in a *nix shell on the remote host, then you will most +likely need to manually set the `execute_command`; The default +`execute_command` used by Packer will not work for you. +When configuring the command you will need to ensure that any dollar signs +or other characters that may be incorrectly interpreted by the remote shell +are escaped accordingly. + +The following example shows how the standard `execute_command` can be +reconfigured to work on a remote system with +[Cygwin/OpenSSH](https://cygwin.com/) installed. +The `execute_command` has each dollar sign backslash escaped so that it is +not interpreted by the remote Bash shell - Bash being the default shell for +Cygwin environments. + +```json + "provisioners": [ + { + "type": "powershell", + "execute_command": "powershell -executionpolicy bypass \"& { if (Test-Path variable:global:ProgressPreference){\\$ProgressPreference='SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit \\$LastExitCode }\"", + "inline": [ + "Write-Host \"Hello from PowerShell\"", + ] + } + ] +``` + + ## Packer's Handling of Characters Special to PowerShell The escape character in PowerShell is the `backtick`, also sometimes