230 lines
9.8 KiB
PowerShell
230 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"
|
|
}
|