diff --git a/packer/provisioner_mock.go b/packer/provisioner_mock.go index 62b304ccb..0c7a25791 100644 --- a/packer/provisioner_mock.go +++ b/packer/provisioner_mock.go @@ -34,3 +34,15 @@ func (t *MockProvisioner) Provision(ui Ui, comm Communicator) error { func (t *MockProvisioner) Cancel() { t.CancelCalled = true } + +func (t *MockProvisioner) Communicator() Communicator { + return t.ProvCommunicator +} + +func (t *MockProvisioner) ElevatedUser() string { + return "user" +} + +func (t *MockProvisioner) ElevatedPassword() string { + return "password" +} diff --git a/provisioner/chef-client/provisioner.go b/provisioner/chef-client/provisioner.go index 8a3bff170..6d451ace4 100644 --- a/provisioner/chef-client/provisioner.go +++ b/provisioner/chef-client/provisioner.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/common/uuid" + commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/provisioner" @@ -50,6 +51,8 @@ type Config struct { ChefEnvironment string `mapstructure:"chef_environment"` ClientKey string `mapstructure:"client_key"` ConfigTemplate string `mapstructure:"config_template"` + ElevatedUser string `mapstructure:"elevated_user"` + ElevatedPassword string `mapstructure:"elevated_password"` EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` ExecuteCommand string `mapstructure:"execute_command"` GuestOSType string `mapstructure:"guest_os_type"` @@ -76,6 +79,7 @@ type Config struct { type Provisioner struct { config Config + communicator packer.Communicator guestOSTypeConfig guestOSTypeConfig guestCommands *provisioner.GuestCommands } @@ -94,6 +98,10 @@ type ConfigTemplate struct { ValidationKeyPath string } +type EnvVarsTemplate struct { + WinRMPassword string +} + type ExecuteTemplate struct { ConfigPath string JsonPath string @@ -111,6 +119,12 @@ type KnifeTemplate struct { } func (p *Provisioner) Prepare(raws ...interface{}) error { + // Create passthrough for winrm password so we can fill it in once we know + // it + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: `{{.WinRMPassword}}`, + } + err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, @@ -221,6 +235,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + p.communicator = comm + nodeName := p.config.NodeName if nodeName == "" { nodeName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) @@ -551,6 +567,13 @@ func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config return err } + if p.config.ElevatedUser != "" { + command, err = provisioner.GenerateElevatedRunner(command, p) + if err != nil { + return err + } + } + ui.Message(fmt.Sprintf("Executing Chef: %s", command)) cmd := &packer.RemoteCmd{ @@ -676,6 +699,31 @@ func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { return result, nil } +func getWinRMPassword(buildName string) string { + winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName) + packer.LogSecretFilter.Set(winRMPass) + return winRMPass +} + +func (p *Provisioner) Communicator() packer.Communicator { + return p.communicator +} + +func (p *Provisioner) ElevatedUser() string { + return p.config.ElevatedUser +} + +func (p *Provisioner) ElevatedPassword() string { + // Replace ElevatedPassword for winrm users who used this feature + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: getWinRMPassword(p.config.PackerBuildName), + } + + elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) + + return elevatedPassword +} + var DefaultConfigTemplate = ` log_level :info log_location STDOUT diff --git a/provisioner/elevated.go b/provisioner/elevated.go new file mode 100644 index 000000000..d34e97e0a --- /dev/null +++ b/provisioner/elevated.go @@ -0,0 +1,192 @@ +package provisioner + +import ( + "bytes" + "encoding/xml" + "fmt" + "log" + "strings" + "text/template" + + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/packer" +) + +type ElevatedProvisioner interface { + Communicator() packer.Communicator + ElevatedUser() string + ElevatedPassword() string +} + +type elevatedOptions struct { + User string + Password string + TaskName string + TaskDescription string + LogFile string + XMLEscapedCommand string +} + +var psEscape = strings.NewReplacer( + "$", "`$", + "\"", "`\"", + "`", "``", + "'", "`'", +) + +var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(` +$name = "{{.TaskName}}" +$log = [System.Environment]::ExpandEnvironmentVariables("{{.LogFile}}") +$s = New-Object -ComObject "Schedule.Service" +$s.Connect() +$t = $s.NewTask($null) +$xml = [xml]@' + + + + {{.TaskDescription}} + + + + {{.User}} + Password + HighestAvailable + + + + IgnoreNew + false + false + true + false + false + + false + false + + true + true + false + false + false + PT24H + 4 + + + + cmd + /c {{.XMLEscapedCommand}} + + + +'@ +$logon_type = 1 +$password = "{{.Password}}" +if ($password.Length -eq 0) { + $logon_type = 5 + $password = $null + $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI) + $node = $xml.SelectSingleNode("/ns:Task/ns:Principals/ns:Principal/ns:LogonType", $ns) + $node.ParentNode.RemoveChild($node) | Out-Null +} +$t.XmlText = $xml.OuterXml +if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"} +$f = $s.GetFolder("\") +$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", $password, $logon_type, $null) | Out-Null +$t = $f.GetTask("\$name") +$t.Run($null) | Out-Null +$timeout = 10 +$sec = 0 +while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) { + Start-Sleep -s 1 + $sec++ +} + +$line = 0 +do { + Start-Sleep -m 100 + if (Test-Path $log) { + Get-Content $log | select -skip $line | ForEach { + $line += 1 + Write-Output "$_" + } + } +} while (!($t.state -eq 3)) +$result = $t.LastTaskResult +if (Test-Path $log) { + Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null +} +[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null +exit $result`)) + +func GenerateElevatedRunner(command string, p ElevatedProvisioner) (uploadedPath string, err error) { + log.Printf("Building elevated command wrapper for: %s", command) + + var buffer bytes.Buffer + + // Output from the elevated command cannot be returned directly to the + // Packer console. In order to be able to view output from elevated + // commands and scripts an indirect approach is used by which the commands + // output is first redirected to file. The output file is then 'watched' + // by Packer while the elevated command is running and any content + // appearing in the file is written out to the console. Below the portion + // of command required to redirect output from the command to file is + // built and appended to the existing command string + taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + // Only use %ENVVAR% format for environment variables when setting the log + // file path; Do NOT use $env:ENVVAR format as it won't be expanded + // correctly in the elevatedTemplate + logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out" + command += fmt.Sprintf(" > %s 2>&1", logFile) + + // elevatedTemplate wraps the command in a single quoted XML text string + // so we need to escape characters considered 'special' in XML. + err = xml.EscapeText(&buffer, []byte(command)) + if err != nil { + return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err) + } + escapedCommand := buffer.String() + log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand) + buffer.Reset() + + // Escape chars special to PowerShell in the ElevatedUser string + elevatedUser := p.ElevatedUser() + escapedElevatedUser := psEscape.Replace(elevatedUser) + if escapedElevatedUser != elevatedUser { + log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", + elevatedUser, escapedElevatedUser) + } + + // Escape chars special to PowerShell in the ElevatedPassword string + elevatedPassword := p.ElevatedPassword() + escapedElevatedPassword := psEscape.Replace(elevatedPassword) + if escapedElevatedPassword != elevatedPassword { + log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", + elevatedPassword, escapedElevatedPassword) + } + + // Generate command + err = elevatedTemplate.Execute(&buffer, elevatedOptions{ + User: escapedElevatedUser, + Password: escapedElevatedPassword, + TaskName: taskName, + TaskDescription: "Packer elevated task", + LogFile: logFile, + XMLEscapedCommand: escapedCommand, + }) + + if err != nil { + fmt.Printf("Error creating elevated template: %s", err) + return "", err + } + uuid := uuid.TimeOrderedUUID() + 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) + } + + return fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path), err +} diff --git a/provisioner/elevated_test.go b/provisioner/elevated_test.go new file mode 100644 index 000000000..ac8759ef2 --- /dev/null +++ b/provisioner/elevated_test.go @@ -0,0 +1,38 @@ +package provisioner + +import ( + "regexp" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "inline": []interface{}{"foo", "bar"}, + } +} + +func TestProvisioner_GenerateElevatedRunner(t *testing.T) { + + // Non-elevated + config := testConfig() + p := new(packer.MockProvisioner) + p.Prepare(config) + comm := new(packer.MockCommunicator) + p.ProvCommunicator = comm + path, err := GenerateElevatedRunner("whoami", p) + + if err != nil { + t.Fatalf("Did not expect error: %s", err.Error()) + } + + if comm.UploadCalled != true { + t.Fatalf("Should have uploaded file") + } + + matched, _ := regexp.MatchString("C:/Windows/Temp/packer-elevated-shell.*", path) + if !matched { + t.Fatalf("Got unexpected file: %s", path) + } +} diff --git a/provisioner/powershell/elevated.go b/provisioner/powershell/elevated.go deleted file mode 100644 index be8dcdf62..000000000 --- a/provisioner/powershell/elevated.go +++ /dev/null @@ -1,100 +0,0 @@ -package powershell - -import ( - "text/template" -) - -type elevatedOptions struct { - User string - Password string - TaskName string - TaskDescription string - LogFile string - XMLEscapedCommand string -} - -var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(` -$name = "{{.TaskName}}" -$log = [System.Environment]::ExpandEnvironmentVariables("{{.LogFile}}") -$s = New-Object -ComObject "Schedule.Service" -$s.Connect() -$t = $s.NewTask($null) -$xml = [xml]@' - - - - {{.TaskDescription}} - - - - {{.User}} - Password - HighestAvailable - - - - IgnoreNew - false - false - true - false - false - - false - false - - true - true - false - false - false - PT24H - 4 - - - - cmd - /c {{.XMLEscapedCommand}} - - - -'@ -$logon_type = 1 -$password = "{{.Password}}" -if ($password.Length -eq 0) { - $logon_type = 5 - $password = $null - $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) - $ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI) - $node = $xml.SelectSingleNode("/ns:Task/ns:Principals/ns:Principal/ns:LogonType", $ns) - $node.ParentNode.RemoveChild($node) | Out-Null -} -$t.XmlText = $xml.OuterXml -if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"} -$f = $s.GetFolder("\") -$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", $password, $logon_type, $null) | Out-Null -$t = $f.GetTask("\$name") -$t.Run($null) | Out-Null -$timeout = 10 -$sec = 0 -while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) { - Start-Sleep -s 1 - $sec++ -} - -$line = 0 -do { - Start-Sleep -m 100 - if (Test-Path $log) { - Get-Content $log | select -skip $line | ForEach { - $line += 1 - Write-Output "$_" - } - } -} while (!($t.state -eq 3)) -$result = $t.LastTaskResult -if (Test-Path $log) { - Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null -} -[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null -exit $result`)) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 3888b3d26..1629c9aa0 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -4,8 +4,6 @@ package powershell import ( "bufio" - "bytes" - "encoding/xml" "errors" "fmt" "io/ioutil" @@ -20,6 +18,7 @@ import ( commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/provisioner" "github.com/hashicorp/packer/template/interpolate" ) @@ -507,90 +506,29 @@ func (p *Provisioner) createCommandTextPrivileged() (command string, err error) return "", fmt.Errorf("Error processing command: %s", err) } - // OK so we need an elevated shell runner to wrap our command, this is - // going to have its own path generate the script and update the command - // runner in the process - path, err := p.generateElevatedRunner(command) + command, err = provisioner.GenerateElevatedRunner(command, p) if err != nil { return "", fmt.Errorf("Error generating elevated runner: %s", err) } - // Return the path to the elevated shell wrapper - command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) - return command, err } -func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { - log.Printf("Building elevated command wrapper for: %s", command) +func (p *Provisioner) Communicator() packer.Communicator { + return p.communicator +} - var buffer bytes.Buffer +func (p *Provisioner) ElevatedUser() string { + return p.config.ElevatedUser +} - // Output from the elevated command cannot be returned directly to the - // Packer console. In order to be able to view output from elevated - // commands and scripts an indirect approach is used by which the commands - // output is first redirected to file. The output file is then 'watched' - // by Packer while the elevated command is running and any content - // appearing in the file is written out to the console. Below the portion - // of command required to redirect output from the command to file is - // built and appended to the existing command string - taskName := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) - // Only use %ENVVAR% format for environment variables when setting the log - // file path; Do NOT use $env:ENVVAR format as it won't be expanded - // correctly in the elevatedTemplate - logFile := `%SYSTEMROOT%/Temp/` + taskName + ".out" - command += fmt.Sprintf(" > %s 2>&1", logFile) - - // elevatedTemplate wraps the command in a single quoted XML text string - // so we need to escape characters considered 'special' in XML. - err = xml.EscapeText(&buffer, []byte(command)) - if err != nil { - return "", fmt.Errorf("Error escaping characters special to XML in command %s: %s", command, err) - } - escapedCommand := buffer.String() - log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand) - buffer.Reset() - - // Escape chars special to PowerShell in the ElevatedUser string - escapedElevatedUser := psEscape.Replace(p.config.ElevatedUser) - if escapedElevatedUser != p.config.ElevatedUser { - log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", - p.config.ElevatedUser, escapedElevatedUser) - } +func (p *Provisioner) ElevatedPassword() string { // Replace ElevatedPassword for winrm users who used this feature p.config.ctx.Data = &EnvVarsTemplate{ WinRMPassword: getWinRMPassword(p.config.PackerBuildName), } - p.config.ElevatedPassword, _ = interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) + elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) - // Escape chars special to PowerShell in the ElevatedPassword string - escapedElevatedPassword := psEscape.Replace(p.config.ElevatedPassword) - if escapedElevatedPassword != p.config.ElevatedPassword { - log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", - p.config.ElevatedPassword, escapedElevatedPassword) - } - - // Generate command - err = elevatedTemplate.Execute(&buffer, elevatedOptions{ - User: escapedElevatedUser, - Password: escapedElevatedPassword, - TaskName: taskName, - TaskDescription: "Packer elevated task", - LogFile: logFile, - XMLEscapedCommand: escapedCommand, - }) - - if err != nil { - fmt.Printf("Error creating elevated template: %s", err) - return "", err - } - uuid := uuid.TimeOrderedUUID() - 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) - } - return path, err + return elevatedPassword } diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go index b0a0a27ab..eef61bf8d 100644 --- a/provisioner/powershell/provisioner_test.go +++ b/provisioner/powershell/provisioner_test.go @@ -654,30 +654,6 @@ func TestProvision_uploadEnvVars(t *testing.T) { } } -func TestProvision_generateElevatedShellRunner(t *testing.T) { - - // Non-elevated - config := testConfig() - p := new(Provisioner) - p.Prepare(config) - comm := new(packer.MockCommunicator) - p.communicator = comm - path, err := p.generateElevatedRunner("whoami") - - if err != nil { - t.Fatalf("Did not expect error: %s", err.Error()) - } - - if comm.UploadCalled != true { - t.Fatalf("Should have uploaded file") - } - - matched, _ := regexp.MatchString("C:/Windows/Temp/packer-elevated-shell.*", path) - if !matched { - t.Fatalf("Got unexpected file: %s", path) - } -} - func TestRetryable(t *testing.T) { config := testConfig() diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go index 3254a6134..8368e37d3 100644 --- a/provisioner/puppet-masterless/provisioner.go +++ b/provisioner/puppet-masterless/provisioner.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/hashicorp/packer/common" + commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/provisioner" @@ -65,6 +66,12 @@ type Config struct { // The directory from which the command will be executed. // Packer requires the directory to exist when running puppet. WorkingDir string `mapstructure:"working_directory"` + + // Instructs the communicator to run the remote script as a Windows + // scheduled task, effectively elevating the remote user by impersonating + // a logged-in user + ElevatedUser string `mapstructure:"elevated_user"` + ElevatedPassword string `mapstructure:"elevated_password"` } type guestOSTypeConfig struct { @@ -117,6 +124,7 @@ var guestOSTypeConfigs = map[string]guestOSTypeConfig{ type Provisioner struct { config Config + communicator packer.Communicator guestOSTypeConfig guestOSTypeConfig guestCommands *provisioner.GuestCommands } @@ -135,7 +143,17 @@ type ExecuteTemplate struct { WorkingDir string } +type EnvVarsTemplate struct { + WinRMPassword string +} + func (p *Provisioner) Prepare(raws ...interface{}) error { + // Create passthrough for winrm password so we can fill it in once we know + // it + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: `{{.WinRMPassword}}`, + } + err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, @@ -240,6 +258,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say("Provisioning with Puppet...") + p.communicator = comm ui.Message("Creating Puppet staging directory...") if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { return fmt.Errorf("Error creating staging directory: %s", err) @@ -316,6 +335,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + if p.config.ElevatedUser != "" { + command, err = provisioner.GenerateElevatedRunner(command, p) + if err != nil { + return err + } + } + cmd := &packer.RemoteCmd{ Command: command, } @@ -432,10 +458,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri } func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("rm -fr '%s'", dir), - } - + cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -460,3 +483,28 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } + +func getWinRMPassword(buildName string) string { + winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName) + packer.LogSecretFilter.Set(winRMPass) + return winRMPass +} + +func (p *Provisioner) Communicator() packer.Communicator { + return p.communicator +} + +func (p *Provisioner) ElevatedUser() string { + return p.config.ElevatedUser +} + +func (p *Provisioner) ElevatedPassword() string { + // Replace ElevatedPassword for winrm users who used this feature + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: getWinRMPassword(p.config.PackerBuildName), + } + + elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) + + return elevatedPassword +} diff --git a/provisioner/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index b7ec34ea7..686bdf847 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/hashicorp/packer/common" + commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/provisioner" @@ -63,6 +64,12 @@ type Config struct { // The directory from which the command will be executed. // Packer requires the directory to exist when running puppet. WorkingDir string `mapstructure:"working_directory"` + + // Instructs the communicator to run the remote script as a Windows + // scheduled task, effectively elevating the remote user by impersonating + // a logged-in user + ElevatedUser string `mapstructure:"elevated_user"` + ElevatedPassword string `mapstructure:"elevated_password"` } type guestOSTypeConfig struct { @@ -112,6 +119,7 @@ var guestOSTypeConfigs = map[string]guestOSTypeConfig{ type Provisioner struct { config Config + communicator packer.Communicator guestOSTypeConfig guestOSTypeConfig guestCommands *provisioner.GuestCommands } @@ -129,7 +137,17 @@ type ExecuteTemplate struct { WorkingDir string } +type EnvVarsTemplate struct { + WinRMPassword string +} + func (p *Provisioner) Prepare(raws ...interface{}) error { + // Create passthrough for winrm password so we can fill it in once we know + // it + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: `{{.WinRMPassword}}`, + } + err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, @@ -210,6 +228,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say("Provisioning with Puppet...") + p.communicator = comm ui.Message("Creating Puppet staging directory...") if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { return fmt.Errorf("Error creating staging directory: %s", err) @@ -269,6 +288,13 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + if p.config.ElevatedUser != "" { + command, err = provisioner.GenerateElevatedRunner(command, p) + if err != nil { + return err + } + } + cmd := &packer.RemoteCmd{ Command: command, } @@ -321,10 +347,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri } func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("rm -fr '%s'", dir), - } - + cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -349,3 +372,28 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } + +func getWinRMPassword(buildName string) string { + winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName) + packer.LogSecretFilter.Set(winRMPass) + return winRMPass +} + +func (p *Provisioner) Communicator() packer.Communicator { + return p.communicator +} + +func (p *Provisioner) ElevatedUser() string { + return p.config.ElevatedUser +} + +func (p *Provisioner) ElevatedPassword() string { + // Replace ElevatedPassword for winrm users who used this feature + p.config.ctx.Data = &EnvVarsTemplate{ + WinRMPassword: getWinRMPassword(p.config.PackerBuildName), + } + + elevatedPassword, _ := interpolate.Render(p.config.ElevatedPassword, &p.config.ctx) + + return elevatedPassword +} diff --git a/website/source/docs/provisioners/chef-client.html.md b/website/source/docs/provisioners/chef-client.html.md index 5c8eb086d..c0ae990b9 100644 --- a/website/source/docs/provisioners/chef-client.html.md +++ b/website/source/docs/provisioners/chef-client.html.md @@ -51,6 +51,11 @@ configuration is actually required. should use a custom configuration template. See the dedicated "Chef Configuration" section below for more details. +- `elevated_user` and `elevated_password` (string) - If specified, Chef will + be run with elevated privileges using the given Windows user. See the + [powershell](/docs/provisionders/powershell.html) provisioner for the full + details. + - `encrypted_data_bag_secret_path` (string) - The path to the file containing the secret for encrypted data bags. By default, this is empty, so no secret will be available. diff --git a/website/source/docs/provisioners/puppet-masterless.html.md b/website/source/docs/provisioners/puppet-masterless.html.md index 1c9ef5307..e28a10e7d 100644 --- a/website/source/docs/provisioners/puppet-masterless.html.md +++ b/website/source/docs/provisioners/puppet-masterless.html.md @@ -115,6 +115,11 @@ manifests you should use `manifest_file` instead. be run. If using Hiera files with relative paths, this option can be helpful. (default: `staging_directory`) +- `elevated_user` and `elevated_password` (string) - If specified, Puppet + will be run with elevated privileges using the given Windows user. See the + [powershell](/docs/provisioners/powershell.html) provisioner for the full + details. + ## Execute Command By default, Packer uses the following command (broken across multiple lines for diff --git a/website/source/docs/provisioners/puppet-server.html.md b/website/source/docs/provisioners/puppet-server.html.md index 1f40ec098..71bb54a9c 100644 --- a/website/source/docs/provisioners/puppet-server.html.md +++ b/website/source/docs/provisioners/puppet-server.html.md @@ -101,6 +101,11 @@ listed below: be run. If using Hiera files with relative paths, this option can be helpful. (default: `staging_directory`) +- `elevated_user` and `elevated_password` (string) - If specified, Puppet + will be run with elevated privileges using the given Windows user. See the + [powershell](/docs/provisioners/powershell.html) provisioner for the full + details. + ## Execute Command By default, Packer uses the following command (broken across multiple lines for