Files
offline_kubespray/collection/ansible/windows/plugins/modules/win_powershell.ps1
ByeonJungHun 360c6eef4a offline 작업
2024-02-19 16:02:29 +09:00

829 lines
29 KiB
PowerShell

#!powershell
# Copyright: (c) 2021, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ..module_utils.Process
$spec = @{
options = @{
arguments = @{ type = 'list'; elements = 'str' }
chdir = @{ type = 'str' }
creates = @{ type = 'str' }
depth = @{ type = 'int'; default = 2 }
error_action = @{ type = 'str'; choices = 'silently_continue', 'continue', 'stop'; default = 'continue' }
executable = @{ type = 'str' }
parameters = @{ type = 'dict' }
removes = @{ type = 'str' }
script = @{ type = 'str'; required = $true }
}
supports_check_mode = $true
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$module.Result.result = @{}
$module.Result.host_out = ''
$module.Result.host_err = ''
$module.Result.output = @()
$module.Result.error = @()
$module.Result.warning = @()
$module.Result.verbose = @()
$module.Result.debug = @()
$module.Result.information = @()
$stdPinvoke = @'
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
namespace Ansible.Windows.WinPowerShell
{
public class NativeMethods
{
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool AllocConsole();
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern IntPtr GetConsoleWindow();
[DllImport("Kernel32.dll")]
public static extern IntPtr GetStdHandle(
int nStdHandle);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool FreeConsole();
[DllImport("Kernel32.dll")]
public static extern bool SetStdHandle(
int nStdHandle,
IntPtr hHandle);
}
}
'@
Add-CSharpType -AnsibleModule $module -References $stdPinvoke, @'
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Security;
namespace Ansible.Windows.WinPowerShell
{
public class Host : PSHost
{
private readonly PSHost PSHost;
private readonly HostUI HostUI;
public Host(PSHost host, StreamWriter stdout, StreamWriter stderr)
{
PSHost = host;
HostUI = new HostUI(stdout, stderr);
}
public override CultureInfo CurrentCulture { get { return PSHost.CurrentCulture; } }
public override CultureInfo CurrentUICulture { get { return PSHost.CurrentUICulture; } }
public override Guid InstanceId { get { return PSHost.InstanceId; } }
public override string Name { get { return PSHost.Name; } }
public override PSHostUserInterface UI { get { return HostUI; } }
public override Version Version { get { return PSHost.Version; } }
public override void EnterNestedPrompt()
{
PSHost.EnterNestedPrompt();
}
public override void ExitNestedPrompt()
{
PSHost.ExitNestedPrompt();
}
public override void NotifyBeginApplication()
{
PSHost.NotifyBeginApplication();
}
public override void NotifyEndApplication()
{
PSHost.NotifyEndApplication();
}
public override void SetShouldExit(int exitCode)
{
PSHost.SetShouldExit(exitCode);
}
}
public class HostUI : PSHostUserInterface
{
private StreamWriter _stdout;
private StreamWriter _stderr;
public HostUI(StreamWriter stdout, StreamWriter stderr)
{
_stdout = stdout;
_stderr = stderr;
}
public override PSHostRawUserInterface RawUI { get { return null; } }
public override Dictionary<string, PSObject> Prompt(string caption, string message, Collection<FieldDescription> descriptions)
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override int PromptForChoice(string caption, string message, Collection<ChoiceDescription> choices, int defaultChoice)
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName,
PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override string ReadLine()
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override SecureString ReadLineAsSecureString()
{
throw new MethodInvocationException("PowerShell is in NonInteractive mode. Read and Prompt functionality is not available.");
}
public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
_stdout.Write(value);
}
public override void Write(string value)
{
_stdout.Write(value);
}
public override void WriteDebugLine(string message)
{
_stdout.WriteLine(String.Format("DEBUG: {0}", message));
}
public override void WriteErrorLine(string value)
{
_stderr.WriteLine(value);
}
public override void WriteLine(string value)
{
_stdout.WriteLine(value);
}
public override void WriteProgress(long sourceId, ProgressRecord record) {}
public override void WriteVerboseLine(string message)
{
_stdout.WriteLine(String.Format("VERBOSE: {0}", message));
}
public override void WriteWarningLine(string message)
{
_stdout.WriteLine(String.Format("WARNING: {0}", message));
}
}
}
'@
Function Get-StdHandle {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateSet('Stdout', 'Stderr')]
[string]
$Stream
)
$id, $dotnet = switch ($Stream) {
Stdout { -11, [Console]::Out }
Stderr { -12, [Console]::Error }
}
$handle = [Ansible.Windows.WinPowerShell.NativeMethods]::GetStdHandle($id)
[PSCustomObject]@{
Stream = $Stream
NET = $dotnet
Raw = $handle
}
}
Function Set-StdHandle {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateSet('Stdout', 'Stderr')]
[string]
$Stream,
[IO.TextWriter]
$NET,
[IntPtr]
$Raw
)
$id, $meth = switch ($Stream) {
Stdout { -11; [Console]::SetOut($NET) }
Stderr { -12; [Console]::SetError($NET) }
}
# .NET does not actually affect the std handle on the process, we need to call SetStdHandle so any child processes
# spawned with Start-Process -NoNewWindow will use our custom pipe.
[void][Ansible.Windows.WinPowerShell.NativeMethods]::SetStdHandle($id, $Raw)
}
Function Convert-OutputObject {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[AllowNull()]
[object]
$InputObject,
[Parameter(Mandatory = $true)]
[int]
$Depth
)
begin {
$childDepth = $Depth - 1
$isType = {
[CmdletBinding()]
param (
[Object]
$InputObject,
[Type]
$Type
)
if ($InputObject -is $Type) {
return $true
}
$psTypes = @($InputObject.PSTypeNames | ForEach-Object -Process {
$_ -replace '^Deserialized.'
})
$Type.FullName -in $psTypes
}
}
process {
if ($null -eq $InputObject) {
$null
}
elseif ((&$isType -InputObject $InputObject -Type ([Enum])) -and $Depth -ge 0) {
# ToString() gives the human readable value but I thought it better to give some more context behind
# these types.
@{
Type = ($InputObject.PSTypeNames[0] -replace '^Deserialized.')
String = $InputObject.ToString()
Value = [int]$InputObject
}
}
elseif ($InputObject -is [DateTime]) {
# The offset is based on the Kind value
# Unspecified leaves it off
# UTC set it to Z
# Local sets it to the local timezone
$InputObject.ToString('o')
}
elseif (&$isType -InputObject $InputObject -Type ([DateTimeOffset])) {
# If this is a deserialized object (from an executable) we need recreate a live DateTimeOffset
if ($InputObject -isnot [DateTimeOffset]) {
$InputObject = New-Object -TypeName DateTimeOffset $InputObject.DateTime, $InputObject.Offset
}
$InputObject.ToString('o')
}
elseif (&$isType -InputObject $InputObject -Type ([Type])) {
if ($Depth -lt 0) {
$InputObject.FullName
}
else {
# This type is very complex with circular properties, only return somewhat useful properties.
# BaseType might be a string (serialized output), try and convert it back to a Type if possible.
$baseType = $InputObject.BaseType -as [Type]
if ($baseType) {
$baseType = Convert-OutputObject -InputObject $baseType -Depth $childDepth
}
@{
Name = $InputObject.Name
FullName = $InputObject.FullName
AssemblyQualifiedName = $InputObject.AssemblyQualifiedName
BaseType = $baseType
}
}
}
elseif ($InputObject -is [string]) {
$InputObject
}
elseif (&$isType -InputObject $InputObject -Type ([switch])) {
$InputObject.IsPresent
}
elseif ($InputObject.GetType().IsValueType) {
# We want to display just this value and not any properties it has (if any).
$InputObject
}
elseif ($Depth -lt 0) {
# This must occur after the above to ensure ints and other ValueTypes are preserved as is.
[string]$InputObject
}
elseif ($InputObject -is [Collections.IList]) {
, @(foreach ($obj in $InputObject) {
Convert-OutputObject -InputObject $obj -Depth $childDepth
})
}
elseif ($InputObject -is [Collections.IDictionary]) {
$newObj = @{}
# Replicate ConvertTo-Json, props are replaced by keys if they share the same name. We only want ETS
# properties as well.
foreach ($prop in $InputObject.PSObject.Properties) {
if ($prop.MemberType -notin @('AliasProperty', 'ScriptProperty', 'NoteProperty')) {
continue
}
$newObj[$prop.Name] = Convert-OutputObject -InputObject $prop.Value -Depth $childDepth
}
foreach ($kvp in $InputObject.GetEnumerator()) {
$newObj[$kvp.Key] = Convert-OutputObject -InputObject $kvp.Value -Depth $childDepth
}
$newObj
}
else {
$newObj = @{}
foreach ($prop in $InputObject.PSObject.Properties) {
$newObj[$prop.Name] = Convert-OutputObject -InputObject $prop.Value -Depth $childDepth
}
$newObj
}
}
}
Function Format-Exception {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[AllowNull()]
$Exception
)
if (-not $Exception) {
return $null
}
elseif ($Exception -is [Management.Automation.RemoteException]) {
# When using a separate process the exceptions are a RemoteException, we want to get the info on the actual
# exception.
return Format-Exception -Exception $Exception.SerializedRemoteException
}
$type = if ($Exception -is [Exception]) {
$Exception.GetType().FullName
}
else {
# This is a RemoteException, we want to report the original non-serialized type.
$Exception.PSTypeNames[0] -replace '^Deserialized.'
}
@{
message = $Exception.Message
type = $type
help_link = $Exception.HelpLink
source = $Exception.Source
hresult = $Exception.HResult
inner_exception = Format-Exception -Exception $Exception.InnerException
}
}
Function Test-AnsiblePath {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[String]
$Path
)
# Certain system files fail on Test-Path due to it being locked, we can still get the attributes though.
try {
[void][System.IO.File]::GetAttributes($Path)
return $true
}
catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] {
return $false
}
catch {
# When testing a path like Cert:\LocalMachine\My, System.IO.File will
# not work, we just revert back to using Test-Path for this
return Test-Path -Path $Path
}
}
Function New-AnonymousPipe {
[CmdletBinding()]
param ()
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
$server = New-Object -TypeName IO.Pipes.AnonymousPipeServerStream -ArgumentList 'In', 'Inheritable'
$client = New-Object -TypeName IO.Pipes.AnonymousPipeClientStream -ArgumentList 'Out', $server.ClientSafePipeHandle
$clientWriter = New-Object -TypeName IO.StreamWriter -ArgumentList $client, $utf8NoBom
$clientWriter.AutoFlush = $true # Ensures the data stays in sync when dealing with subprocesses.
# Create the background task that will constantly read from the pipe and append to our StringBuilder until the pipe
# is closed. It also closes the pipe once finished so we don't have to. Without this the pipe buffer can become
# full and hang the script.
$sb = New-Object -TypeName Text.StringBuilder
$ps = [PowerShell]::Create()
[void]$ps.AddScript(@'
[CmdletBinding()]
param (
[Text.StringBuilder]
$StringBuilder,
[IO.Pipes.AnonymousPipeServerStream]
$Server,
[Text.Encoding]
$Encoding
)
$sr = New-Object -TypeName IO.StreamReader -ArgumentList $Server, $Encoding
try {
$buffer = New-Object -TypeName char[] -ArgumentList $Server.InBufferSize
while ($read = $sr.Read($buffer, 0, $buffer.Length)) {
[void]$StringBuilder.Append($buffer, 0, $read)
}
}
finally {
$sr.Dispose()
}
'@).AddParameters(@{
StringBuilder = $sb
Server = $server
Encoding = $utf8NoBom
})
$task = $ps.BeginInvoke()
[PSCustomObject]@{
PowerShell = $ps
Task = $task
Output = $sb
Client = $clientWriter
ClientString = $server.GetClientHandleAsString()
}
}
$creates = $module.Params.creates
if ($creates -and (Test-AnsiblePath -Path $creates)) {
$module.Result.msg = "skipped, since $creates exists"
$module.ExitJson()
}
$removes = $module.Params.removes
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
$module.Result.msg = "skipped, since $removes does not exist"
$module.ExitJson()
}
# Check if the script has [CmdletBinding(SupportsShouldProcess)] on it
$scriptAst = [ScriptBlock]::Create($module.Params.script).Ast
$supportsShouldProcess = $false
if ($scriptAst -is [Management.Automation.Language.ScriptBlockAst] -and $scriptAst.ParamBlock.Attributes) {
$supportsShouldProcess = [bool]($scriptAst.ParamBlock.Attributes |
Where-Object { $_.TypeName.Name -eq 'CmdletBinding' } |
Select-Object -First 1 |
ForEach-Object -Process {
$_.NamedArguments | Where-Object {
$_.ArgumentName -eq 'SupportsShouldProcess' -and ($_.ExpressionOmitted -or $_.Argument.ToString() -eq '$true')
}
})
}
if ($module.CheckMode -and -not $supportsShouldProcess) {
$module.Result.changed = $true
$module.Result.msg = "skipped, running in check mode"
$module.ExitJson()
}
$runspace = $null
$processId = $null
$newStdout = New-AnonymousPipe
$newStderr = New-AnonymousPipe
$freeConsole = $false
try {
$oldStdout = Get-StdHandle -Stream Stdout
$oldStderr = Get-StdHandle -Stream Stderr
if ($module.Params.executable) {
if ($PSVersionTable.PSVersion -lt [version]'5.0') {
$module.FailJson("executable requires PowerShell 5.0 or newer")
}
# Neither Start-Process or Diagnostics.Process give us the ability to create a process with a new console and
# the ability to inherit handles so we use own home grown CreateProcess wrapper.
$applicationName = Resolve-ExecutablePath -FilePath $module.Params.executable
$commandLine = ConvertTo-EscapedArgument -InputObject $module.Params.executable
if ($module.Params.arguments) {
$escapedArguments = @($module.Params.arguments | ConvertTo-EscapedArgument)
$commandLine += " $($escapedArguments -join ' ')"
}
# While we could attach the stdout/stderr pipes here we would capture the startup info and prompt that
# powershell will output. Instead we set the console as part of the pipeline we run.
$si = [Ansible.Windows.Process.StartupInfo]@{
WindowStyle = 'Hidden' # Useful when debugging locally, doesn't really matter in normal Ansible.
}
$pi = [Ansible.Windows.Process.ProcessUtil]::NativeCreateProcess(
$applicationName,
$commandLIne,
$null,
$null,
$true, # Required so the child process can inherit our anon pipes.
'CreateNewConsole', # Ensures we don't mess with the current console output.
$null,
$null,
$si
)
$processId = $pi.ProcessId
$pi.Dispose()
}
# Using a custom host allows us to capture any host UI calls through our own output.
$runspaceHost = New-Object -TypeName Ansible.Windows.WinPowerShell.Host -ArgumentList $Host, $newStdout.Client, $newStderr.Client
if ($processId) {
$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]$processId
# In case a user specified an executable that does not support the PSHost named pipe that PowerShell uses we
# specify a timeout so the module does not hang.
$connInfo.OpenTimeout = 5000
$runspace = [RunspaceFactory]::CreateRunspace($runspaceHost, $connInfo)
}
else {
$runspace = [RunspaceFactory]::CreateRunspace($runspaceHost)
}
$runspace.Open()
$ps = [PowerShell]::Create()
$ps.Runspace = $runspace
$ps.Runspace.SessionStateProxy.SetVariable('Ansible', [PSCustomObject]@{
PSTypeName = 'Ansible.Windows.WinPowerShell.Module'
CheckMode = $module.CheckMode
Verbosity = $module.Verbosity
Result = @{}
Changed = $true
Failed = $false
Tmpdir = $module.Tmpdir
})
$eap = switch ($module.Params.error_action) {
'stop' { 'Stop' }
'continue' { 'Continue' }
'silently_continue' { 'SilentlyContinue' }
}
$ps.Runspace.SessionStateProxy.SetVariable('ErrorActionPreference', [Management.Automation.ActionPreference]$eap)
if ($processId) {
# If we are running in a new process we need to set the various console encoding values to UTF-8 to ensure a
# consistent encoding experience when PowerShell is running native commands and getting the output back. We
# also need to redirect the stdout/stderr pipes to our anonymous pipe so we can capture any native console
# output from .NET or calling a native application with 'Start-Process -NoNewWindow'.
[void]$ps.AddScript(@'
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[String]
$StdoutHandle,
[Parameter(Mandatory=$true)]
[String]
$StderrHandle,
[Parameter(Mandatory=$true)]
[String]
$SetStdPInvoke,
[Parameter(Mandatory=$true)]
[String]
$SetScriptBlock,
[Parameter(Mandatory=$true)]
[String]
$AddTypeCode,
[Parameter(Mandatory=$true)]
[String]
$Tmpdir
)
# Using Add-Type here leaves an empty folder for some reason, our code does not and also allows us to control the
# temp directory used.
&([ScriptBlock]::Create($AddTypeCode)) -References $SetStdPInvoke -TempPath $Tmpdir
$setHandle = [ScriptBlock]::Create($SetScriptBlock)
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
# Make sure our console encoding values are all set to UTF-8 for a consistent experience.
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = $utf8NoBom
# Set the stdout/stderr for both .NET and natively to our anonymous pipe.
@{Name = 'Stdout'; Handle = $StdoutHandle}, @{Name = 'Stderr'; Handle = $StderrHandle} | ForEach-Object -Process {
$pipe = New-Object -TypeName IO.Pipes.AnonymousPipeClientStream -ArgumentList 'Out', $_.Handle
$writer = New-Object -TypeName IO.StreamWriter -ArgumentList $pipe, $utf8NoBom
$writer.AutoFlush = $true # Ensures we data in the correct order.
&$setHandle -Stream $_.Name -NET $writer -Raw $pipe.SafePipeHandle.DangerousGetHandle()
}
'@, $true).AddParameters(@{
StdoutHandle = $newStdout.ClientString
StderrHandle = $newStderr.ClientString
SetStdPInvoke = $stdPinvoke
SetScriptBlock = ${function:Set-StdHandle}
AddTypeCode = ${function:Add-CSharpType}
TmpDir = $module.Tmpdir
}).AddStatement()
}
else {
# The psrp connection plugin doesn't have a console so we need to create one ourselves.
if ([Ansible.Windows.WinPowerShell.NativeMethods]::GetConsoleWindow() -eq [IntPtr]::Zero) {
$freeConsole = [Ansible.Windows.WinPowerShell.NativeMethods]::AllocConsole()
}
# Else we are running in the same process, we need to redirect the console and .NET output pipes to our
# anonymous pipe. We shouldn't have to set the encoding, the module wrapper already does this.
Set-StdHandle -Stream Stdout -NET $newStdout.Client -Raw $newStdout.Client.BaseStream.SafePipeHandle.DangerousGetHandle()
Set-StdHandle -Stream Stderr -NET $newStderr.Client -Raw $newStderr.Client.BaseStream.SafePipeHandle.DangerousGetHandle()
$utf8NoBom = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false
$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = $utf8NoBom
}
if ($module.Params.chdir) {
[void]$ps.AddCommand('Set-Location').AddParameter('LiteralPath', $module.Params.chdir).AddStatement()
}
[void]$ps.AddScript($module.Params.script)
# We copy the existing parameter dictionary and add/modify the Confirm/WhatIf parameters if the script supports
# processing. We do a copy to avoid modifying the original Params dictionary just for safety.
$parameters = @{}
if ($module.Params.parameters) {
foreach ($kvp in $module.Params.parameters.GetEnumerator()) {
$parameters[$kvp.Key] = $kvp.Value
}
}
if ($supportsShouldProcess) {
# We do this last to ensure we take precedence over any user inputted settings.
$parameters.Confirm = $false # Ensure we don't block on any confirmation prompts
$parameters.WhatIf = $module.CheckMode
}
if ($parameters) {
[void]$ps.AddParameters($parameters)
}
# We cannot natively call a generic function so need to resort to reflection to get the method we know is there
# and turn it into an invocable method. We do this so we can call the overload that takes in an IList for the
# output which means we don't loose anything from before a terminating exception was raised.
$psOutput = [Collections.Generic.List[Object]]@()
$invokeMethod = $ps.GetType().GetMethods('Public, Instance, InvokeMethod') | Where-Object {
if ($_.Name -ne 'Invoke' -or $_.ReturnType -ne [void] -or -not $_.ContainsGenericParameters) {
return $false
}
$parameters = $_.GetParameters()
(
$parameters.Count -eq 2 -and
$parameters[0].ParameterType -eq [Collections.IEnumerable] -and
$parameters[1].ParameterType.Namespace -eq 'System.Collections.Generic' -and
$parameters[1].ParameterType.Name -eq 'IList`1'
)
}
$invoke = $invokeMethod.MakeGenericMethod([object])
try {
$invoke.Invoke($ps, @(@(), $psOutput))
}
catch [Management.Automation.RuntimeException] {
# $ErrorActionPrefrence = 'Stop' and an error was raised
# OR
# General exception was raised in the script like 'throw "error"'.
# We treat these as failures in the script and return them back to the user.
$module.Result.failed = $true
$ps.Streams.Error.Add($_.Exception.ErrorRecord)
}
# Get the internal Ansible variable that can contain code specific information.
$result = $runspace.SessionStateProxy.GetVariable('Ansible')
}
finally {
if (-not $processId) {
$oldStdout, $oldStderr | ForEach-Object -Process {
Set-StdHandle -Stream $_.Stream -NET $_.NET -Raw $_.Raw
}
}
if ($runspace) {
$runspace.Dispose()
}
if ($processId) {
Stop-Process -Id $processId -Force
}
$newStdout, $newStderr | ForEach-Object -Process {
$_.Client.Dispose()
[void]$_.PowerShell.EndInvoke($_.Task)
}
if ($freeConsole) {
[void][Ansible.Windows.WinPowerShell.NativeMethods]::FreeConsole()
}
}
$module.Result.host_out = $newStdout.Output.ToString()
$module.Result.host_err = $newStderr.Output.ToString()
$module.Result.result = Convert-OutputObject -InputObject $result.Result -Depth $module.Params.depth
$module.Result.changed = $result.Changed
$module.Result.failed = $module.Result.failed -or $result.Failed
# We process the output outselves to flatten anything beyond the depth and deal with certain problematic types with
# json serialization.
$module.Result.output = Convert-OutputObject -InputObject $psOutput -Depth $module.Params.depth
$module.Result.error = @($ps.Streams.Error | ForEach-Object -Process {
$err = @{
output = ($_ | Out-String)
error_details = $null
exception = Format-Exception -Exception $_.Exception
target_object = $_.TargetObject
category_info = @{
category = [string]$_.CategoryInfo.Category
category_id = [int]$_.CategoryInfo.Category
activity = $_.CategoryInfo.Activity
reason = $_.CategoryInfo.Reason
target_name = $_.CategoryInfo.TargetName
target_type = $_.CategoryInfo.TargetType
}
fully_qualified_error_id = $_.FullyQualifiedErrorId
script_stack_trace = $_.ScriptStackTrace
pipeline_iteration_info = $_.PipelineIterationInfo
}
if ($_.ErrorDetails) {
$err.error_details = @{
message = $_.ErrorDetails.Message
recommended_action = $_.ErrorDetails.RecommendedAction
}
}
$err
})
'debug', 'verbose', 'warning' | ForEach-Object -Process {
$module.Result.$_ = @($ps.Streams.$_ | Select-Object -ExpandProperty Message)
}
# Use Select-Object as Information may not be present on earlier pwsh version (<v5).
$module.Result.information = @($ps.Streams |
Select-Object -ExpandProperty Information -ErrorAction SilentlyContinue |
ForEach-Object -Process {
@{
message_data = Convert-OutputObject -InputObject $_.MessageData -Depth $module.Params.depth
source = $_.Source
time_generated = $_.TimeGenerated.ToUniversalTime().ToString('o')
tags = @($_.Tags)
}
}
)
$module.ExitJson()