Merge pull request #6160 from DanHam/fix-powershell-ssh

Fixes for PowerShell using ssh communicator
pull/6183/head
M. Marsh 8 years ago committed by GitHub
commit 52df315ca9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
}

@ -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)
}

@ -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

Loading…
Cancel
Save