mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-25 21:44:00 -07:00 
			
		
		
		
	* prevent scriptblock logging from logging payload contents * added tests to verify no payload contents in PS Operational event log * fix script action to send split-aware wrapper * fix CLIXML error parser (return to -EncodedCommand exposed problems with it)
		
			
				
	
	
		
			229 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			PowerShell
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			PowerShell
		
	
	
	
	
	
| # (c) 2018 Ansible Project
 | |
| # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 | |
| 
 | |
| begin {
 | |
|     $DebugPreference = "Continue"
 | |
|     $ProgressPreference = "SilentlyContinue"
 | |
|     $ErrorActionPreference = "Stop"
 | |
|     Set-StrictMode -Version 2
 | |
| 
 | |
|     # common functions that are loaded in exec and module context, this is set
 | |
|     # as a script scoped variable so async_watchdog and module_wrapper can
 | |
|     # access the functions when creating their Runspaces
 | |
|     $script:common_functions = {
 | |
|         Function ConvertFrom-AnsibleJson {
 | |
|             <#
 | |
|             .SYNOPSIS
 | |
|             Converts a JSON string to a Hashtable/Array in the fastest way
 | |
|             possible. Unfortunately ConvertFrom-Json is still faster but outputs
 | |
|             a PSCustomObject which is combersone for module consumption.
 | |
| 
 | |
|             .PARAMETER InputObject
 | |
|             [String] The JSON string to deserialize.
 | |
|             #>
 | |
|             param(
 | |
|                 [Parameter(Mandatory=$true, Position=0)][String]$InputObject
 | |
|             )
 | |
| 
 | |
|             # we can use -AsHashtable to get PowerShell to convert the JSON to
 | |
|             # a Hashtable and not a PSCustomObject. This was added in PowerShell
 | |
|             # 6.0, fall back to a manual conversion for older versions
 | |
|             $cmdlet = Get-Command -Name ConvertFrom-Json -CommandType Cmdlet
 | |
|             if ("AsHashtable" -in $cmdlet.Parameters.Keys) {
 | |
|                 return ,(ConvertFrom-Json -InputObject $InputObject -AsHashtable)
 | |
|             } else {
 | |
|                 # get the PSCustomObject and then manually convert from there
 | |
|                 $raw_obj = ConvertFrom-Json -InputObject $InputObject
 | |
| 
 | |
|                 Function ConvertTo-Hashtable {
 | |
|                     param($InputObject)
 | |
| 
 | |
|                     if ($null -eq $InputObject) {
 | |
|                         return $null
 | |
|                     }
 | |
| 
 | |
|                     if ($InputObject -is [PSCustomObject]) {
 | |
|                         $new_value = @{}
 | |
|                         foreach ($prop in $InputObject.PSObject.Properties.GetEnumerator()) {
 | |
|                             $new_value.($prop.Name) = (ConvertTo-Hashtable -InputObject $prop.Value)
 | |
|                         }
 | |
|                         return ,$new_value
 | |
|                     } elseif ($InputObject -is [Array]) {
 | |
|                         $new_value = [System.Collections.ArrayList]@()
 | |
|                         foreach ($val in $InputObject) {
 | |
|                             $new_value.Add((ConvertTo-Hashtable -InputObject $val)) > $null
 | |
|                         }
 | |
|                         return ,$new_value.ToArray()
 | |
|                     } else {
 | |
|                         return ,$InputObject
 | |
|                     }
 | |
|                 }
 | |
|                 return ,(ConvertTo-Hashtable -InputObject $raw_obj)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         Function Format-AnsibleException {
 | |
|             <#
 | |
|             .SYNOPSIS
 | |
|             Formats a PowerShell ErrorRecord to a string that's fit for human
 | |
|             consumption.
 | |
| 
 | |
|             .NOTES
 | |
|             Using Out-String can give us the first part of the exception but it
 | |
|             also wraps the messages at 80 chars which is not ideal. We also
 | |
|             append the ScriptStackTrace and the .NET StackTrace if present.
 | |
|             #>
 | |
|             param([System.Management.Automation.ErrorRecord]$ErrorRecord)
 | |
| 
 | |
|             $exception = @"
 | |
| $($ErrorRecord.ToString())
 | |
| $($ErrorRecord.InvocationInfo.PositionMessage)
 | |
|     + CategoryInfo          : $($ErrorRecord.CategoryInfo.ToString())
 | |
|     + FullyQualifiedErrorId : $($ErrorRecord.FullyQualifiedErrorId.ToString())
 | |
| "@
 | |
|             # module_common strip comments and empty newlines, need to manually
 | |
|             # add a preceding newline using `r`n
 | |
|             $exception += "`r`n`r`nScriptStackTrace:`r`n$($ErrorRecord.ScriptStackTrace)`r`n"
 | |
| 
 | |
|             # exceptions from C# will also have a StackTrace which we
 | |
|             # append if found
 | |
|             if ($null -ne $ErrorRecord.Exception.StackTrace) {
 | |
|                 $exception += "`r`n$($ErrorRecord.Exception.ToString())"
 | |
|             }
 | |
| 
 | |
|             return $exception
 | |
|         }
 | |
|     }
 | |
|     .$common_functions
 | |
| 
 | |
|     # common wrapper functions used in the exec wrappers, this is defined in a
 | |
|     # script scoped variable so async_watchdog can pass them into the async job
 | |
|     $script:wrapper_functions = {
 | |
|         Function Write-AnsibleError {
 | |
|             <#
 | |
|             .SYNOPSIS
 | |
|             Writes an error message to a JSON string in the format that Ansible
 | |
|             understands. Also optionally adds an exception record if the
 | |
|             ErrorRecord is passed through.
 | |
|             #>
 | |
|             param(
 | |
|                 [Parameter(Mandatory=$true)][String]$Message,
 | |
|                 [System.Management.Automation.ErrorRecord]$ErrorRecord = $null
 | |
|             )
 | |
|             $result = @{
 | |
|                 msg = $Message
 | |
|                 failed = $true
 | |
|             }
 | |
|             if ($null -ne $ErrorRecord) {
 | |
|                 $result.msg += ": $($ErrorRecord.Exception.Message)"
 | |
|                 $result.exception = (Format-AnsibleException -ErrorRecord $ErrorRecord)
 | |
|             }
 | |
|             Write-Output -InputObject (ConvertTo-Json -InputObject $result -Depth 99 -Compress)
 | |
|         }
 | |
| 
 | |
|         Function Write-AnsibleLog {
 | |
|             <#
 | |
|             .SYNOPSIS
 | |
|             Used as a debugging tool to log events to a file as they run in the
 | |
|             exec wrappers. By default this is a noop function but the $log_path
 | |
|             can be manually set to enable it. Manually set ANSIBLE_EXEC_DEBUG as
 | |
|             an env value on the Windows host that this is run on to enable.
 | |
|             #>
 | |
|             param(
 | |
|                 [Parameter(Mandatory=$true, Position=0)][String]$Message,
 | |
|                 [Parameter(Position=1)][String]$Wrapper
 | |
|             )
 | |
| 
 | |
|             $log_path = $env:ANSIBLE_EXEC_DEBUG
 | |
|             if ($log_path) {
 | |
|                 $log_path = [System.Environment]::ExpandEnvironmentVariables($log_path)
 | |
|                 $parent_path = [System.IO.Path]::GetDirectoryName($log_path)
 | |
|                 if (Test-Path -LiteralPath $parent_path -PathType Container) {
 | |
|                     $msg = "{0:u} - {1} - {2} - " -f (Get-Date), $pid, ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
 | |
|                     if ($null -ne $Wrapper) {
 | |
|                         $msg += "$Wrapper - "
 | |
|                     }
 | |
|                     $msg += $Message + "`r`n"
 | |
|                     $msg_bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
 | |
| 
 | |
|                     $fs = [System.IO.File]::Open($log_path, [System.IO.FileMode]::Append,
 | |
|                         [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
 | |
|                     try {
 | |
|                         $fs.Write($msg_bytes, 0, $msg_bytes.Length)
 | |
|                     } finally {
 | |
|                         $fs.Close()
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     .$wrapper_functions
 | |
| 
 | |
|     # only init and stream in $json_raw if it wasn't set by the enclosing scope
 | |
|     if (-not $(Get-Variable "json_raw" -ErrorAction SilentlyContinue)) {
 | |
|         $json_raw = ''
 | |
|     }
 | |
| } process {
 | |
|     $json_raw += [String]$input
 | |
| } end {
 | |
|     Write-AnsibleLog "INFO - starting exec_wrapper" "exec_wrapper"
 | |
|     if (-not $json_raw) {
 | |
|         Write-AnsibleError -Message "internal error: no input given to PowerShell exec wrapper"
 | |
|         exit 1
 | |
|     }
 | |
| 
 | |
|     Write-AnsibleLog "INFO - converting json raw to a payload" "exec_wrapper"
 | |
|     $payload = ConvertFrom-AnsibleJson -InputObject $json_raw
 | |
| 
 | |
|     # TODO: handle binary modules
 | |
|     # TODO: handle persistence
 | |
| 
 | |
|     if ($payload.min_os_version) {
 | |
|         $min_os_version = [Version]$payload.min_os_version
 | |
|         # Environment.OSVersion.Version is deprecated and may not return the
 | |
|         # right version
 | |
|         $actual_os_version = [Version](Get-Item -Path $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion
 | |
| 
 | |
|         Write-AnsibleLog "INFO - checking if actual os version '$actual_os_version' is less than the min os version '$min_os_version'" "exec_wrapper"
 | |
|         if ($actual_os_version -lt $min_os_version) {
 | |
|             Write-AnsibleError -Message "internal error: This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version"
 | |
|             exit 1
 | |
|         }
 | |
|     }
 | |
|     if ($payload.min_ps_version) {
 | |
|         $min_ps_version = [Version]$payload.min_ps_version
 | |
|         $actual_ps_version = $PSVersionTable.PSVersion
 | |
| 
 | |
|         Write-AnsibleLog "INFO - checking if actual PS version '$actual_ps_version' is less than the min PS version '$min_ps_version'" "exec_wrapper"
 | |
|         if ($actual_ps_version -lt $min_ps_version) {
 | |
|             Write-AnsibleError -Message "internal error: This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version"
 | |
|             exit 1
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     # pop 0th action as entrypoint
 | |
|     $action = $payload.actions[0]
 | |
|     Write-AnsibleLog "INFO - running action $action" "exec_wrapper"
 | |
| 
 | |
|     $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.($action)))
 | |
|     $entrypoint = [ScriptBlock]::Create($entrypoint)
 | |
|     # so we preserve the formatting and don't fall prey to locale issues, some
 | |
|     # wrappers want the output to be in base64 form, we store the value here in
 | |
|     # case the wrapper changes the value when they create a payload for their
 | |
|     # own exec_wrapper
 | |
|     $encoded_output = $payload.encoded_output
 | |
| 
 | |
|     try {
 | |
|         $output = &$entrypoint -Payload $payload
 | |
|         if ($encoded_output -and $null -ne $output) {
 | |
|             $b64_output = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($output))
 | |
|             Write-Output -InputObject $b64_output
 | |
|         } else {
 | |
|             Write-Output -InputObject $output
 | |
|         }
 | |
|     } catch {
 | |
|         Write-AnsibleError -Message "internal error: failed to run exec_wrapper action $action" -ErrorRecord $_
 | |
|         exit 1
 | |
|     }
 | |
|     Write-AnsibleLog "INFO - ending exec_wrapper" "exec_wrapper"
 | |
| }
 |