summaryrefslogtreecommitdiffstats
path: root/lib/ansible/executor/powershell/exec_wrapper.ps1
blob: 0f97bdfb8a5b6b5c2109c875fc73f7beab724aeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# (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) {
            $msg = "internal error: This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version"
            Write-AnsibleError -Message $msg
            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) {
            $msg = "internal error: This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version"
            Write-AnsibleError -Message $msg
            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 {
            $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"
}