From e0fc3e52e2284ee3a287c806164f7dc9adcece7f Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Fri, 28 Jan 2022 23:24:50 +0100 Subject: [PATCH] Adds support for check_by_icingaforwindows.ps1 --- doc/100-General/10-Changelog.md | 1 + .../Invoke-IcingaInternalServiceCall.psm1 | 28 ++++-- .../plugin/Exit-IcingaExecutePlugin.psm1 | 96 +++++++++++++++---- .../Write-IcingaExecutePluginException.psm1 | 38 ++++++++ 4 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 lib/icinga/plugin/Write-IcingaExecutePluginException.psm1 diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 5c195a2..3784b7a 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -47,6 +47,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic * [#445](https://github.com/Icinga/icinga-powershell-framework/pull/445) Adds command `Repair-IcingaService` to repair Icinga Agent service in case it was broken during upgrades, mostly caused by `The specified service has been marked for deletion` * [#448](https://github.com/Icinga/icinga-powershell-framework/pull/448) Adds support to sort arrays without ScriptBlocks * [#450](https://github.com/Icinga/icinga-powershell-framework/pull/450) Improves show command `Show-IcingaRegisteredServiceChecks`, adds new command `Show-IcingaRegisteredBackgroundDaemons` and extends `Show-Icinga` by both commands and adds debug and api forwarder features to environment list +* [#455](https://github.com/Icinga/icinga-powershell-framework/pull/455) Adds support for remote execution plugin [check_by_icingaforwindows](https://github.com/LordHepipud/check_by_icingaforwindows) ## 1.7.1 (2021-11-11) diff --git a/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 b/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 index 0432b9b..899d529 100644 --- a/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 +++ b/lib/core/framework/Invoke-IcingaInternalServiceCall.psm1 @@ -2,31 +2,32 @@ function Invoke-IcingaInternalServiceCall() { param ( [string]$Command = '', - [array]$Arguments = @() + [array]$Arguments = @(), + [switch]$NoExit = $FALSE ); # If our Framework is running as daemon, never call our api if ($Global:Icinga.Protected.RunAsDaemon) { - return; + return $NULL; } # If the API forward feature is disabled, do nothing if ((Get-IcingaFrameworkApiChecks) -eq $FALSE) { - return; + return $NULL; } # Test our Icinga for Windows service. If the service is not installed or not running, execute the plugin locally $IcingaForWindowsService = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue); if ($null -eq $IcingaForWindowsService -Or $IcingaForWindowsService.Status -ne 'Running') { - return; + return $NULL; } # In case the REST-Api module ist not configured, do nothing $BackgroundDaemons = Get-IcingaBackgroundDaemons; if ($null -eq $BackgroundDaemons -Or $BackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi') -eq $FALSE) { - return; + return $NULL; } $RestApiPort = 5668; @@ -57,6 +58,11 @@ function Invoke-IcingaInternalServiceCall() [string]$Argument = [string]$Value; $ArgumentValue = $null; + if ($Argument -eq '-IcingaForWindowsRemoteExecution' -Or $Argument -eq '-IcingaForWindowsJEARemoteExecution') { + $ArgumentIndex += 1; + continue; + } + if ($Value[0] -eq '-') { if (($ArgumentIndex + 1) -lt $Arguments.Count) { [string]$NextValue = $Arguments[$ArgumentIndex + 1]; @@ -85,7 +91,7 @@ function Invoke-IcingaInternalServiceCall() } catch { # Fallback to execute plugin locally Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -ExceptionObject $_ -Objects $Command, $CommandArguments; - return; + return $NULL; } # Resolve our result from the API @@ -95,12 +101,12 @@ function Invoke-IcingaInternalServiceCall() # In case we didn't receive a check result, fallback to local execution if ([string]::IsNullOrEmpty($IcingaResult.$Command.checkresult)) { Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $CommandArguments; - return; + return $NULL; } if ([string]::IsNullOrEmpty($IcingaResult.$Command.exitcode)) { Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $CommandArguments; - return; + return $NULL; } $IcingaCR = ($IcingaResult.$Command.checkresult.Replace("`r`n", "`n")); @@ -112,6 +118,12 @@ function Invoke-IcingaInternalServiceCall() } } + if ($NoExit) { + Set-IcingaInternalPluginExitCode -ExitCode $IcingaResult.$Command.exitcode; + + return $IcingaCR; + } + # Print our response and exit with the provide exit code Write-IcingaConsolePlain $IcingaCR; exit $IcingaResult.$Command.exitcode; diff --git a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 index 2b2634a..e7c93eb 100644 --- a/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 +++ b/lib/icinga/plugin/Exit-IcingaExecutePlugin.psm1 @@ -4,34 +4,103 @@ function Exit-IcingaExecutePlugin() [string]$Command = '' ); - $JEAProfile = Get-IcingaJEAContext; + [string]$JEAProfile = Get-IcingaJEAContext; + [bool]$CheckByIcingaForWindows = $FALSE; + [bool]$CheckByJEAShell = $FALSE; + if ($args -Contains '-IcingaForWindowsRemoteExecution') { + $CheckByIcingaForWindows = $TRUE; + } + if ($args -Contains '-IcingaForWindowsJEARemoteExecution') { + $CheckByJEAShell = $TRUE; + } + + # We use the plugin check_by_icingaforwindows.ps1 to execute + # checks from a Linux/Windows remote source + if ($CheckByIcingaForWindows) { + # First try to queue the check over the REST-Api + $CheckResult = Invoke-IcingaInternalServiceCall -Command $Command -Arguments $args -NoExit; + + if ($null -ne $CheckResult) { + # Seems we got a result + Write-IcingaConsolePlain -Message $CheckResult; + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # We couldn't use our Rest-Api and Api-Checks feature, then lets execute the plugin locally + # Set daemon true, because this will change internal handling for errors and plugin output + $Global:Icinga.Protected.RunAsDaemon = $TRUE; + + try { + # Execute our plugin + (& $Command @args) | Out-Null; + } catch { + # Handle errors within our plugins + # If anything goes wrong handle the error very detailed + + $Global:Icinga.Protected.RunAsDaemon = $FALSE; + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $args; + $args.Clear(); + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # Disable it again - we need to write data to our shell now. Not very intuitive, but it is the easiest + # solution to do it this way + $Global:Icinga.Protected.RunAsDaemon = $FALSE; + + # Now print the result to shell + Write-IcingaPluginResult -PluginOutput (Get-IcingaInternalPluginOutput) -PluginPerfData (Get-IcingaCheckSchedulerPerfData); + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # Regardless of JEA enabled or disabled, forward all checks to the internal API + # and check if we get a result from there Invoke-IcingaInternalServiceCall -Command $Command -Arguments $args; try { + # If the plugin is not installed, throw a good exception Exit-IcingaPluginNotInstalled -Command $Command; + # In case we have JEA enabled on our system, this shell currently open most likely has no + # JEA configuration installed. This is because a JEA shell will not return an exit code and + # Icinga relies on that. Therefor we will try to open a new PowerShell with the JEA configuration + # assigned for Icinga for Windows, execute the plugins there and return the result if ([string]::IsNullOrEmpty($JEAProfile) -eq $FALSE) { $ErrorHandler = '' $JEARun = ( & powershell.exe -ConfigurationName $JEAProfile -NoLogo -NoProfile -Command { + # Load Icinga for Windows Use-Icinga; + # Enable our JEA context $Global:Icinga.Protected.JEAContext = $TRUE; + # Parse the arguments our previous shell received $Command = $args[0]; $Arguments = $args[1]; $Output = ''; try { + # Try executing our checks, store the exit code and plugin output $ExitCode = (& $Command @Arguments); $Output = (Get-IcingaInternalPluginOutput); $ExitCode = (Get-IcingaInternalPluginExitCode); } catch { + # If we failed for some reason, print a detailed error and use exit code 3 to mark the check as unkown $Output = [string]::Format('[UNKNOWN] Icinga Exception: Error while executing plugin in JEA context{0}{0}{1}', (New-IcingaNewLine), $_.Exception.Message); $ExitCode = 3; } + # Return the result to our main PowerShell return @{ 'Output' = $Output; 'PerfData' = (Get-IcingaCheckSchedulerPerfData) @@ -40,34 +109,27 @@ function Exit-IcingaExecutePlugin() } -args $Command, $args ) 2>$ErrorHandler; + # If we have an exit code larger or equal 0, the execution inside the JEA shell was successfully and we can share the result + # In case we had an error inside the JEA shell, it will returned here as well if ($LASTEXITCODE -ge 0) { Write-IcingaPluginResult -PluginOutput $JEARun.Output -PluginPerfData $JEARun.PerfData; exit $JEARun.ExitCode; } else { + # If for some reason the PowerShell could not be started within JEA context, we can throw an exception with exit code 3 + # to mark the check as unknown including our error message Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: Unable to start the PowerShell.exe with the provided JEA profile "{0}" for CheckCommand: {1}' -Objects $JEAProfile, $Command; exit 3; } } else { + # If we simply run the check without JEA context or from remote, we can just execute the plugin and + # exit with the exit code received from the result exit (& $Command @args); } } catch { - $ExMsg = $_.Exception.Message; - $StackTrace = $_.ScriptStackTrace; - $ExErrorId = $_.FullyQualifiedErrorId; - $ArgName = $_.Exception.ParameterName; - $ListArgs = $args; + # If anything goes wrong handle the error + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $args; + $args.Clear(); - if ($ExErrorId -Like "*ParameterArgumentTransformationError*" -And $ExMsg.Contains('System.Security.SecureString')) { - $ExMsg = [string]::Format( - 'Cannot bind parameter {0}. Cannot convert the provided value for argument "{0}" of type "System.String" to type "System.Security.SecureString".', - $ArgName - ); - - $args.Clear(); - $ListArgs = 'Hidden for security reasons'; - } - - Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: {0}{1}{1}CheckCommand: {2}{1}Arguments: {3}{1}{1}StackTrace:{1}{4}' -Objects $ExMsg, (New-IcingaNewLine), $Command, $ListArgs, $StackTrace; exit 3; } } diff --git a/lib/icinga/plugin/Write-IcingaExecutePluginException.psm1 b/lib/icinga/plugin/Write-IcingaExecutePluginException.psm1 new file mode 100644 index 0000000..42c1dc4 --- /dev/null +++ b/lib/icinga/plugin/Write-IcingaExecutePluginException.psm1 @@ -0,0 +1,38 @@ +function Write-IcingaExecutePluginException() +{ + param ( + $Command = '', + $ErrorObject = $null, + $Arguments = @() + ); + + if ($null -eq $ErrorObject) { + return; + } + + $ExMsg = $ErrorObject.Exception.Message; + $StackTrace = $ErrorObject.ScriptStackTrace; + $ExErrorId = $ErrorObject.FullyQualifiedErrorId; + $ArgName = $ErrorObject.Exception.ParameterName; + $ListArgs = @(); + + foreach ($entry in $Arguments) { + if ($entry -eq '-IcingaForWindowsRemoteExecution' -Or $entry -eq '-IcingaForWindowsJEARemoteExecution') { + continue; + } + $ListArgs += $entry; + } + + if ($ExErrorId -Like "*ParameterArgumentTransformationError*" -And $ExMsg.Contains('System.Security.SecureString')) { + $ExMsg = [string]::Format( + 'Cannot bind parameter {0}. Cannot convert the provided value for argument "{0}" of type "System.String" to type "System.Security.SecureString".', + $ArgName + ); + + $Arguments.Clear(); + $ListArgs = 'Hidden for security reasons'; + } + + Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: {0}{1}{1}CheckCommand: {2}{1}Arguments: {3}{1}{1}StackTrace:{1}{4}' -Objects $ExMsg, (New-IcingaNewLine), $Command, $ListArgs, $StackTrace; + $Global:Icinga.Private.Scheduler.ExitCode = 3; +}