diff options
Diffstat (limited to 'lib/ansible/executor/powershell/module_wrapper.ps1')
-rw-r--r-- | lib/ansible/executor/powershell/module_wrapper.ps1 | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/lib/ansible/executor/powershell/module_wrapper.ps1 b/lib/ansible/executor/powershell/module_wrapper.ps1 new file mode 100644 index 0000000..20a9677 --- /dev/null +++ b/lib/ansible/executor/powershell/module_wrapper.ps1 @@ -0,0 +1,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) + } +} |