From 11be4ffc4b3bb3a8055c9cb623dfbf1f00d784df Mon Sep 17 00:00:00 2001 From: Matt Dainty Date: Thu, 6 Dec 2018 18:00:22 +0000 Subject: [PATCH] Attempt at generalising elevated support Refactor puppet-server provisioner to use it. --- provisioner/elevated.go | 192 +++++++++++++++++++++++ provisioner/puppet-server/elevated.go | 100 ------------ provisioner/puppet-server/provisioner.go | 99 ++---------- 3 files changed, 201 insertions(+), 190 deletions(-) create mode 100644 provisioner/elevated.go delete mode 100644 provisioner/puppet-server/elevated.go 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/puppet-server/elevated.go b/provisioner/puppet-server/elevated.go deleted file mode 100644 index 5890a2aa0..000000000 --- a/provisioner/puppet-server/elevated.go +++ /dev/null @@ -1,100 +0,0 @@ -package puppetserver - -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/puppet-server/provisioner.go b/provisioner/puppet-server/provisioner.go index 4612606fe..686bdf847 100644 --- a/provisioner/puppet-server/provisioner.go +++ b/provisioner/puppet-server/provisioner.go @@ -3,16 +3,12 @@ package puppetserver import ( - "bytes" - "encoding/xml" "fmt" - "log" "os" "path/filepath" "strings" "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" @@ -20,13 +16,6 @@ import ( "github.com/hashicorp/packer/template/interpolate" ) -var psEscape = strings.NewReplacer( - "$", "`$", - "\"", "`\"", - "`", "``", - "'", "`'", -) - type Config struct { common.PackerConfig `mapstructure:",squash"` ctx interpolate.Context @@ -300,7 +289,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } if p.config.ElevatedUser != "" { - command, err = p.createCommandTextPrivileged(command) + command, err = provisioner.GenerateElevatedRunner(command, p) if err != nil { return err } @@ -390,91 +379,21 @@ func getWinRMPassword(buildName string) string { return winRMPass } -func (p *Provisioner) createCommandTextPrivileged(input string) (output string, err error) { - // 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(input) - if err != nil { - return "", fmt.Errorf("Error generating elevated runner: %s", err) - } - - // Return the path to the elevated shell wrapper - output = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) - - return output, err +func (p *Provisioner) Communicator() packer.Communicator { + return p.communicator } -func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { - log.Printf("Building elevated command wrapper for: %s", command) +func (p *Provisioner) ElevatedUser() string { + return p.config.ElevatedUser +} - 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 - 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 }