summaryrefslogtreecommitdiffstats
path: root/lib/ansible/executor/powershell/module_wrapper.ps1
blob: 20a967731b769b27611a9cfc2a08f571dc988a83 (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
# (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

<#
.SYNOPSIS
Invokes an Ansible module in a new Runspace. This cmdlet will output the
module's output and write any errors to the error stream of the current
host.

.PARAMETER Scripts
[Object[]] String or ScriptBlocks to execute.

.PARAMETER Variables
[System.Collections.ArrayList] The variables to set in the new Pipeline.
Each value is a hashtable that contains the parameters to use with
Set-Variable;
    Name: the name of the variable to set
    Value: the value of the variable to set
    Scope: the scope of the variable

.PARAMETER Environment
[System.Collections.IDictionary] A Dictionary of environment key/values to
set in the new Pipeline.

.PARAMETER Modules
[System.Collections.IDictionary] A Dictionary of PowerShell modules to
import into the new Pipeline. The key is the name of the module and the
value is a base64 string of the module util code.

.PARAMETER ModuleName
[String] The name of the module that is being executed.

.PARAMETER Breakpoints
A list of line breakpoints to add to the runspace debugger. This is used to
track module and module_utils coverage.
#>
param(
    [Object[]]$Scripts,
    [System.Collections.ArrayList][AllowEmptyCollection()]$Variables,
    [System.Collections.IDictionary]$Environment,
    [System.Collections.IDictionary]$Modules,
    [String]$ModuleName,
    [System.Management.Automation.LineBreakpoint[]]$Breakpoints = @()
)

Write-AnsibleLog "INFO - creating new PowerShell pipeline for $ModuleName" "module_wrapper"
$ps = [PowerShell]::Create()

# do not set ErrorActionPreference for script
if ($ModuleName -ne "script") {
    $ps.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
}

# force input encoding to preamble-free UTF8 so PS sub-processes (eg,
# Start-Job) don't blow up. This is only required for WinRM, a PSRP
# runspace doesn't have a host console and this will bomb out
if ($host.Name -eq "ConsoleHost") {
    Write-AnsibleLog "INFO - setting console input encoding to UTF8 for $ModuleName" "module_wrapper"
    $ps.AddScript('[Console]::InputEncoding = New-Object Text.UTF8Encoding $false').AddStatement() > $null
}

# set the variables
foreach ($variable in $Variables) {
    Write-AnsibleLog "INFO - setting variable '$($variable.Name)' for $ModuleName" "module_wrapper"
    $ps.AddCommand("Set-Variable").AddParameters($variable).AddStatement() > $null
}

# set the environment vars
if ($Environment) {
    # Escaping quotes can be problematic, instead just pass the string to the runspace and set it directly.
    Write-AnsibleLog "INFO - setting environment vars for $ModuleName" "module_wrapper"
    $ps.Runspace.SessionStateProxy.SetVariable("_AnsibleEnvironment", $Environment)
    $ps.AddScript(@'
foreach ($env_kv in $_AnsibleEnvironment.GetEnumerator()) {
    [System.Environment]::SetEnvironmentVariable($env_kv.Key, $env_kv.Value)
}
'@).AddStatement() > $null
}

# import the PS modules
if ($Modules) {
    foreach ($module in $Modules.GetEnumerator()) {
        Write-AnsibleLog "INFO - create module util '$($module.Key)' for $ModuleName" "module_wrapper"
        $module_name = $module.Key
        $module_code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($module.Value))
        $ps.AddCommand("New-Module").AddParameters(@{Name = $module_name; ScriptBlock = [ScriptBlock]::Create($module_code) }) > $null
        $ps.AddCommand("Import-Module").AddParameter("WarningAction", "SilentlyContinue") > $null
        $ps.AddCommand("Out-Null").AddStatement() > $null
    }
}

# redefine Write-Host to dump to output instead of failing
# lots of scripts still use it
$ps.AddScript('Function Write-Host($msg) { Write-Output -InputObject $msg }').AddStatement() > $null

# add the scripts and run
foreach ($script in $Scripts) {
    $ps.AddScript($script).AddStatement() > $null
}

if ($Breakpoints.Count -gt 0) {
    Write-AnsibleLog "INFO - adding breakpoint to runspace that will run the modules" "module_wrapper"
    if ($PSVersionTable.PSVersion.Major -eq 3) {
        # The SetBreakpoints method was only added in PowerShell v4+. We need to rely on a private method to
        # achieve the same functionality in this older PowerShell version. This should be removed once we drop
        # support for PowerShell v3.
        $set_method = $ps.Runspace.Debugger.GetType().GetMethod(
            'AddLineBreakpoint', [System.Reflection.BindingFlags]'Instance, NonPublic'
        )
        foreach ($b in $Breakpoints) {
            $set_method.Invoke($ps.Runspace.Debugger, [Object[]]@(, $b)) > $null
        }
    }
    else {
        $ps.Runspace.Debugger.SetBreakpoints($Breakpoints)
    }
}

Write-AnsibleLog "INFO - start module exec with Invoke() - $ModuleName" "module_wrapper"

# temporarily override the stdout stream and create our own in a StringBuilder
# we use this to ensure there's always an Out pipe and that we capture the
# output for things like async or psrp
$orig_out = [System.Console]::Out
$sb = New-Object -TypeName System.Text.StringBuilder
$new_out = New-Object -TypeName System.IO.StringWriter -ArgumentList $sb
try {
    [System.Console]::SetOut($new_out)
    $module_output = $ps.Invoke()
}
catch {
    # uncaught exception while executing module, present a prettier error for
    # Ansible to parse
    $error_params = @{
        Message = "Unhandled exception while executing module"
        ErrorRecord = $_
    }

    # Be more defensive when trying to find the InnerException in case it isn't
    # set. This shouldn't ever be the case but if it is then it makes it more
    # difficult to track down the problem.
    if ($_.Exception.PSObject.Properties.Name -contains "InnerException") {
        $inner_exception = $_.Exception.InnerException
        if ($inner_exception.PSObject.Properties.Name -contains "ErrorRecord") {
            $error_params.ErrorRecord = $inner_exception.ErrorRecord
        }
    }

    Write-AnsibleError @error_params
    $host.SetShouldExit(1)
    return
}
finally {
    [System.Console]::SetOut($orig_out)
    $new_out.Dispose()
}

# other types of errors may not throw an exception in Invoke but rather just
# set the pipeline state to failed
if ($ps.InvocationStateInfo.State -eq "Failed" -and $ModuleName -ne "script") {
    $reason = $ps.InvocationStateInfo.Reason
    $error_params = @{
        Message = "Unhandled exception while executing module"
    }

    # The error record should always be set on the reason but this does not
    # always happen on Server 2008 R2 for some reason (probably memory hotfix).
    # Be defensive when trying to get the error record and fall back to other
    # options.
    if ($null -eq $reason) {
        $error_params.Message += ": Unknown error"
    }
    elseif ($reason.PSObject.Properties.Name -contains "ErrorRecord") {
        $error_params.ErrorRecord = $reason.ErrorRecord
    }
    else {
        $error_params.Message += ": $($reason.ToString())"
    }

    Write-AnsibleError @error_params
    $host.SetShouldExit(1)
    return
}

Write-AnsibleLog "INFO - module exec ended $ModuleName" "module_wrapper"
$stdout = $sb.ToString()
if ($stdout) {
    Write-Output -InputObject $stdout
}
if ($module_output.Count -gt 0) {
    # do not output if empty collection
    Write-AnsibleLog "INFO - using the output stream for module output - $ModuleName" "module_wrapper"
    Write-Output -InputObject ($module_output -join "`r`n")
}

# we attempt to get the return code from the LASTEXITCODE variable
# this is set explicitly in newer style variables when calling
# ExitJson and FailJson. If set we set the current hosts' exit code
# to that same value
$rc = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE")
if ($null -ne $rc) {
    Write-AnsibleLog "INFO - got an rc of $rc from $ModuleName exec" "module_wrapper"
    $host.SetShouldExit($rc)
}

# PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback
# with the trap handler that's now in place, this should only write to the output if
# $ErrorActionPreference != "Stop", that's ok because this is sent to the stderr output
# for a user to manually debug if something went horribly wrong
if ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) {
    Write-AnsibleLog "WARN - module had errors, outputting error info $ModuleName" "module_wrapper"
    # if the rc wasn't explicitly set, we return an exit code of 1
    if ($null -eq $rc) {
        $host.SetShouldExit(1)
    }

    # output each error to the error stream of the current pipeline
    foreach ($err in $ps.Streams.Error) {
        $error_msg = Format-AnsibleException -ErrorRecord $err

        # need to use the current hosts's UI class as we may not have
        # a console to write the stderr to, e.g. psrp
        Write-AnsibleLog "WARN - error msg for for $($ModuleName):`r`n$error_msg" "module_wrapper"
        $host.UI.WriteErrorLine($error_msg)
    }
}