From 4e7feea184fc3b157b9b59fc534fc4db47387d39 Mon Sep 17 00:00:00 2001 From: DanHam Date: Fri, 23 Mar 2018 11:46:55 +0000 Subject: [PATCH 1/8] Allow users to specify the location that the env vars file is uploaded to Previously the location the file was uploaded to was set internally and used ${env:SYSTEMROOT}/Temp as the destination folder. By default, in order to inject the required environment variables, the file is 'dot sourced' by the 'execute_command' using the {{ .Vars }} variable. Unfortunately the inclusion of the dollar in the path caused issues for users connecting over ssh as the (typically bash) shell running the execute command would try and interpret the dollar sign. The change allows users to specify the location the file is uploaded to, thereby allowing the user to specify a custom 'execute_command' that escapes any dollar signs that could be present in the path. If not set the upload path now defaults to using C:/Windows/Temp as the upload folder. --- provisioner/powershell/provisioner.go | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index c5a4845f7..8bcdaded4 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) From 5b652316d9a8c8f88b19a790a14835f798c32926 Mon Sep 17 00:00:00 2001 From: DanHam Date: Fri, 23 Mar 2018 12:26:48 +0000 Subject: [PATCH 2/8] Dollar's in env vars used in paths may cause problems for ssh * Dollars are interpreted by *nix shells so paths using env vars such as ${env:SYSTEMROOT} will cause issues --- provisioner/powershell/provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 8bcdaded4..8ec58c776 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -565,7 +565,7 @@ 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 { From 54193754132f51f4eee572d7bf98353bfc951e68 Mon Sep 17 00:00:00 2001 From: DanHam Date: Fri, 23 Mar 2018 12:33:35 +0000 Subject: [PATCH 3/8] Change to CMD % style path is no longer required --- provisioner/powershell/provisioner.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 8ec58c776..82ec98c82 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -571,8 +571,5 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin 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 } From ccf687dac6ca27a98d8c932d4a930fc30710f8e9 Mon Sep 17 00:00:00 2001 From: DanHam Date: Wed, 18 Apr 2018 02:14:21 +0100 Subject: [PATCH 4/8] Fix tests --- provisioner/powershell/provisioner_test.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) 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) } From 5db82aab215f350d29927e4a722ce85898447418 Mon Sep 17 00:00:00 2001 From: DanHam Date: Wed, 18 Apr 2018 11:18:47 +0100 Subject: [PATCH 5/8] Doc option allowing override of the path the env var script is uploaded to --- .../source/docs/provisioners/powershell.html.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/source/docs/provisioners/powershell.html.md b/website/source/docs/provisioners/powershell.html.md index 0a97d41ac..b4e1d2ed8 100644 --- a/website/source/docs/provisioners/powershell.html.md +++ b/website/source/docs/provisioners/powershell.html.md @@ -115,6 +115,20 @@ Optional parameters: 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_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 exists in order to deal with times when SSH may restart, such as a From 2779fb0042da063b81155d13c0073155268dc3a6 Mon Sep 17 00:00:00 2001 From: DanHam Date: Thu, 19 Apr 2018 15:50:12 +0100 Subject: [PATCH 6/8] Additional information for using PowerShell with SSH --- .../docs/provisioners/powershell.html.md | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/website/source/docs/provisioners/powershell.html.md b/website/source/docs/provisioners/powershell.html.md index b4e1d2ed8..24083da68 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 @@ -161,6 +165,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 From 8bbc9be71395043c44d39181e5c9a18e2bf8f511 Mon Sep 17 00:00:00 2001 From: DanHam Date: Wed, 18 Apr 2018 12:07:56 +0100 Subject: [PATCH 7/8] Explain how to manually set {{.Path}} and {{.Vars}} --- website/source/docs/provisioners/powershell.html.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/source/docs/provisioners/powershell.html.md b/website/source/docs/provisioners/powershell.html.md index 24083da68..e1de273ec 100644 --- a/website/source/docs/provisioners/powershell.html.md +++ b/website/source/docs/provisioners/powershell.html.md @@ -101,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 From f17523401e9d07a4a6c75c3f085a470483ba5852 Mon Sep 17 00:00:00 2001 From: DanHam Date: Wed, 18 Apr 2018 12:20:52 +0100 Subject: [PATCH 8/8] The default path for uploading the PS script now incorporates a UUID --- website/source/docs/provisioners/powershell.html.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/website/source/docs/provisioners/powershell.html.md b/website/source/docs/provisioners/powershell.html.md index e1de273ec..f5b19529d 100644 --- a/website/source/docs/provisioners/powershell.html.md +++ b/website/source/docs/provisioners/powershell.html.md @@ -117,9 +117,14 @@ 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