Merge pull request #383 from Icinga:feature/move_restapi_apichecks_into_framework

Feature: Move REST-Api and Aoi-Checks into Framework

As the long-term goal will be to move away from check execution with local PowerShell instances, we move the REST-Api and Api-Checks feature directly into the Framework.

By adding everything directly into the Framework, we ensure that no side-loading of modules is required and we can use the API and checker features to increase performance by a lot and provide direct setup and installation procedures inside the IMC.
This commit is contained in:
Lord Hepipud 2021-10-27 10:10:06 +02:00 committed by GitHub
commit 65f9f948d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1032 additions and 0 deletions

View file

@ -4,6 +4,19 @@ Upgrading Icinga PowerShell Framework is usually quite straightforward.
Specific version upgrades are described below. Please note that version updates are incremental.
## Upgrading to v1.7.0 (2021-11-09)
### REST-Api and Api-Checks
With Icinga for Windows v1.7.0, the previously separate available components REST-Api [icinga-powershell-restapi](https://icinga.com/docs/icinga-for-windows/latest/restapi/doc/01-Introduction/) and API-Checks [icinga-powershell-apichecks](https://icinga.com/docs/icinga-for-windows/latest/apichecks/doc/01-Introduction/) are now directly baked into the Icinga PowerShell Framework. You will no longer require to install these components in addition.
**Upgrading**: If you previously installed these components, you should remove them from the system before actively using Icinga for Windows v1.7.0, as additional changes were made in this case.
```powershell
Uninstall-IcingaComponent -Name 'restapi';
Uninstall-IcingaComponent -Name 'apichecks';
```
## Upgrading to v1.5.0 (2021-06-02)
### `SecureString` and Icinga Director Baskets

View file

@ -17,6 +17,10 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic
* [#376](https://github.com/Icinga/icinga-powershell-framework/pull/376) Fixes IMC error handling on invalid JSON for installation command/file
* [#381](https://github.com/Icinga/icinga-powershell-framework/issues/381) Fixes Repository Hash generator for new repositories, which always returned the same hash regardless of the files inside
### Enhancements
* [#383](https://github.com/Icinga/icinga-powershell-framework/pull/383) Moves the components REST-Api [icinga-powershell-restapi](https://icinga.com/docs/icinga-for-windows/latest/restapi/doc/01-Introduction/) and API-Checks [icinga-powershell-apichecks](https://icinga.com/docs/icinga-for-windows/latest/apichecks/doc/01-Introduction/) directly into the Framework
## 1.6.1 (2021-09-15)
[Issue and PRs](https://github.com/Icinga/icinga-powershell-framework/milestone/21?closed=1)

View file

@ -0,0 +1,24 @@
function Add-IcingaRESTClientBlacklistCount()
{
param (
[System.Net.Sockets.TcpClient]$Client = $null,
$ClientList = $null
);
if ($null -eq $Client) {
return;
}
[string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client;
[string]$IpAddress = $Endpoint.Split(':')[0];
[int]$Value = Get-IcingaHashtableItem `
-Hashtable $ClientList `
-Key $IpAddress `
-NullValue 0;
Add-IcingaHashtableItem `
-Hashtable $ClientList `
-Key $IpAddress `
-Value ($Value + 1) `
-Override | Out-Null;
}

View file

@ -0,0 +1,55 @@
function Invoke-IcingaRESTAPIv1Calls()
{
param (
[Hashtable]$Request = @{},
[Hashtable]$Connection = @{}
);
[string]$ModuleToLoad = Get-IcingaRESTPathElement -Request $Request -Index 1;
# Map our Icinga globals to a shorter variable
$RestDaemon = $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi;
if ([string]::IsNullOrEmpty($ModuleToLoad)) {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) `
-ContentBody @{
'Endpoints' = @(
$RestDaemon.RegisteredEndpoints.Keys
)
}
) -Stream $Connection.Stream;
return;
}
if ($RestDaemon.RegisteredEndpoints.ContainsKey($ModuleToLoad) -eq $FALSE) {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Not Found') `
-ContentBody 'There was no module found which is registered for this endpoint name.'
) -Stream $Connection.Stream;
return;
}
[string]$Command = $RestDaemon.RegisteredEndpoints[$ModuleToLoad];
Write-IcingaDebugMessage -Message 'Executing REST-Module' -Objects $Command;
if ($null -eq (Get-Command $Command -ErrorAction SilentlyContinue)) {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Internal Server Error') `
-ContentBody 'This API endpoint is registered, but the PowerShell Cmdlet this module is referencing too does not exist. Please check if the module was installed correctly and contact the developer if you require assistance to resolve this issue.'
) -Stream $Connection.Stream;
return;
}
[hashtable]$CommandArguments = @{
'-Request' = $Request;
'-Connection' = $Connection;
'-IcingaGlobals' = $IcingaDaemonData;
'-ApiVersion' = 'v1';
};
& $Command @CommandArguments | Out-Null;
}

View file

@ -0,0 +1,18 @@
function Remove-IcingaRESTClientBlacklist()
{
param (
[System.Net.Sockets.TcpClient]$Client = $null,
$ClientList = $null
);
if ($null -eq $Client) {
return;
}
[string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client;
[string]$IpAddress = $Endpoint.Split(':')[0];
Remove-IcingaHashtableItem `
-Hashtable $ClientList `
-Key $IpAddress;
}

View file

@ -0,0 +1,26 @@
function Test-IcingaRESTClientBlacklisted()
{
param (
[System.Net.Sockets.TcpClient]$Client = $null,
$ClientList = $null
);
if ($null -eq $Client) {
return $FALSE;
}
[string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client;
[string]$IpAddress = $Endpoint.Split(':')[0];
[int]$Value = Get-IcingaHashtableItem `
-Hashtable $ClientList `
-Key $IpAddress `
-NullValue 0;
# After 9 invalid attempts we will blacklist the client
if ($Value -gt 9) {
Write-IcingaDebugMessage -Message 'Client is blacklisted' -Objects $IpAddress, $Value, $Client.Client;
return $TRUE;
}
return $FALSE;
}

View file

@ -0,0 +1,22 @@
function Test-IcingaRESTClientConnection()
{
param(
[Hashtable]$Connection = @{}
);
# If we couldn't establish a proper SSL stream, close the connection
# immediately without opening a client connection thread
if ($null -eq $Connection.Stream) {
Add-IcingaRESTClientBlacklistCount `
-Client $Connection.Client `
-ClientList $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi.ClientBlacklist;
Write-IcingaEventMessage -EventId 1501 -Namespace 'Framework' -Objects $Connection.Client.Client;
Close-IcingaTCPConnection -Client $Connection.Client;
$Connection = $null;
return $FALSE;
}
Write-IcingaDebugMessage 'Client connection has passed Test';
return $TRUE;
}

View file

@ -0,0 +1,164 @@
function New-IcingaForWindowsRESTApi()
{
# Allow us to parse the framework global data to this thread
param (
$IcingaDaemonData,
$Port,
$RootFolder,
$CertFile,
$CertThumbprint,
$RequireAuth
);
# Import the framework library components and initialise it
# as daemon
Use-Icinga -LibOnly -Daemon;
$Global:IcingaDaemonData = $IcingaDaemonData;
# Add a synchronized hashtable to the global data background
# daemon hashtable to write data to. In addition it will
# allow to share data collected from this daemon with others
$IcingaDaemonData.BackgroundDaemon.Add(
'IcingaPowerShellRestApi',
[hashtable]::Synchronized(@{})
);
# Map our Icinga globals to a shorter variable
$RestDaemon = $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi;
# This will add another hashtable to our previous
# IcingaPowerShellRestApi hashtable to store actual
# endpoint configurations for the API
$RestDaemon.Add(
'RegisteredEndpoints',
[hashtable]::Synchronized(@{})
);
# This will add another hashtable to our previous
# IcingaPowerShellRestApi hashtable to store actual
# command aliases for execution for the API
$RestDaemon.Add(
'CommandAliases',
[hashtable]::Synchronized(@{})
);
# This will add another hashtable to our previous
# IcingaPowerShellRestApi hashtable to store actual
# command aliases for execution for the API
$RestDaemon.Add(
'ClientBlacklist',
[hashtable]::Synchronized(@{})
);
# Make the root folder of our rest daemon module available
# for every possible thread we require
$RestDaemon.Add(
'RootFolder', $RootFolder
);
$RESTEndpoints = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaRESTAPIEndpoint*';
Write-IcingaDebugMessage -Message (
[string]::Format(
'Loading configuration for REST-Endpoints{0}{1}',
(New-IcingaNewLine),
($RESTEndpoints | Out-String)
)
);
Write-IcingaDebugMessage -Message ($RestDaemon | Out-String);
foreach ($entry in $RESTEndpoints.Values) {
[bool]$Success = Add-IcingaHashtableItem -Hashtable $RestDaemon.RegisteredEndpoints `
-Key $entry.Alias `
-Value $entry.Command;
if ($Success -eq $FALSE) {
Write-IcingaEventMessage `
-EventId 2100 `
-Namespace 'RESTApi' `
-Objects ([string]::Format('Adding duplicated REST endpoint "{0}" with command "{1}', $entry.Alias, $entry.Command)), $RESTEndpoints;
}
}
$CommandAliases = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaRESTApiCommandAliases*';
foreach ($entry in $CommandAliases.Values) {
foreach ($component in $entry.Keys) {
[bool]$Success = Add-IcingaHashtableItem -Hashtable $RestDaemon.CommandAliases `
-Key $component `
-Value $entry[$component];
if ($Success -eq $FALSE) {
Write-IcingaEventMessage `
-EventId 2101 `
-Namespace 'RESTApi' `
-Objects ([string]::Format('Adding duplicated REST command aliases "{0}" for namespace "{1}', $entry[$component], $component)), $CommandAliases;
}
}
}
Write-IcingaDebugMessage -Message ($RestDaemon.RegisteredEndpoints | Out-String);
if ($Global:IcingaDaemonData.JEAContext) {
if ($global:IcingaDaemonData.ContainsKey('SSLCertificate') -eq $FALSE -Or $null -eq $global:IcingaDaemonData.SSLCertificate) {
Write-IcingaEventMessage -EventId 2001 -Namespace 'RESTApi';
return;
}
$Certificate = $global:IcingaDaemonData.SSLCertificate;
} else {
$Certificate = Get-IcingaSSLCertForSocket -CertFile $CertFile -CertThumbprint $CertThumbprint;
}
if ($null -eq $Certificate) {
Write-IcingaEventMessage -EventId 2000 -Namespace 'RESTApi';
return;
}
$Socket = New-IcingaTCPSocket -Address $Address -Port $Port -Start;
# Keep our code executed as long as the PowerShell service is
# being executed. This is required to ensure we will execute
# the code frequently instead of only once
while ($TRUE) {
$Connection = Open-IcingaTCPClientConnection `
-Client (New-IcingaTCPClient -Socket $Socket) `
-Certificate $Certificate;
if (Test-IcingaRESTClientBlacklisted -Client $Connection.Client -ClientList $RestDaemon.ClientBlacklist) {
Write-IcingaDebugMessage -Message 'A remote client which is trying to connect was blacklisted' -Objects $Connection.Client.Client;
Close-IcingaTCPConnection -Client $Connection.Client;
continue;
}
if ((Test-IcingaRESTClientConnection -Connection $Connection) -eq $FALSE) {
continue;
}
# API not yet ready
if ($IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.Count -eq 0) {
Close-IcingaTCPConnection -Client $Connection.Client;
continue;
}
try {
$NextRESTApiThreadId = (Get-IcingaNextRESTApiThreadId);
if ($IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.ContainsKey($NextRESTApiThreadId) -eq $FALSE) {
Close-IcingaTCPConnection -Client $Connection.Client;
continue;
}
$IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.$NextRESTApiThreadId.Enqueue($Connection);
} catch {
$ExMsg = $_.Exception.Message;
Write-IcingaEventMessage -Namespace 'RESTApi' -EvenId 2050 -Objects $ExMsg;
}
# Cleanup the error stack and remove not required data
$Error.Clear();
# Force PowerShell to call the garbage collector to free memory
[System.GC]::Collect();
}
}

View file

@ -0,0 +1,93 @@
<#
.SYNOPSIS
A background daemon for Icinga for Windows, providing a REST-Api interface over secure
TLS (https) connections. As certificates either custom ones are used or by default the
Icinga Agent certificates
.DESCRIPTION
This background daemon provides a REST-Api interface by default on Port 5668 over
TLS connections. No unencrypted http requests are allowed. As certificate it will
either use the default Icinga Agent certificates (default config) or custom generated
ones, specified as arguments while installing the daemon.
In addition you can enable authentication, which allows local users or domain users.
By default 5 concurrent threads will perform the work on API requests, can how ever be
reduced or increased depending on configuration.
More Information on
https://icinga.com/docs/icinga-for-windows/latest/restapi
.PARAMETER Address
Allows to specify on which Address a socket should be created on. Defaults to loopback if empty
.PARAMETER Port
The Port the REST-Api will listen on. Defaults to 5668
.PARAMETER CertFile
Use this to define a path on your local disk of you are using custom certificates.
Supported files are: .pfx, .crt
By default, the local Icinga Agent certificates are used
.PARAMETER CertThumbprint
Provide a thumbprint of a certificate stored within the local Windows Cert store
By default, the local Icinga Agent certificates are used
.PARAMETER RequireAuth
Enable authentication which will add basic auth prompt for any request on the API.
For authentication you can either use local Windows accounts or domain accounts
.PARAMETER ConcurrentThreads
Defines on how many threads are started to process API requests. Defaults to 5
.PARAMETER Timeout
Not for use on this module directly, but allows other modules and features to properly
get an idea after which time interval connections are terminated
.LINK
https://github.com/Icinga/icinga-powershell-restapi
.NOTES
#>
function Start-IcingaWindowsRESTApi()
{
param (
[string]$Address = '',
[int]$Port = 5668,
[string]$CertFile = $null,
[string]$CertThumbprint = $null,
[bool]$RequireAuth = $FALSE,
[int]$ConcurrentThreads = 5,
[int]$Timeout = 30
);
$RootFolder = $PSScriptRoot;
$global:IcingaDaemonData.IcingaThreadContent.Add('RESTApi', ([hashtable]::Synchronized(@{})));
$global:IcingaDaemonData.IcingaThreadPool.Add('IcingaRESTApi', (New-IcingaThreadPool -MaxInstances ($ThreadId + 3)));
$global:IcingaDaemonData.IcingaThreadContent.RESTApi.Add('ApiRequests', ([hashtable]::Synchronized(@{})));
$global:IcingaDaemonData.IcingaThreadContent.RESTApi.Add('ApiCallThreadAssignment', ([hashtable]::Synchronized(@{})));
$global:IcingaDaemonData.IcingaThreadContent.RESTApi.Add('TotalThreads', $ConcurrentThreads);
$global:IcingaDaemonData.IcingaThreadContent.RESTApi.Add('LastThreadId', 0);
# Now create a new thread for our REST-Api, assign a name and parse all required arguments to it.
# Last but not least start it directly
New-IcingaThreadInstance `
-Name 'Icinga_for_Windows_REST_Api' `
-ThreadPool $global:IcingaDaemonData.IcingaThreadPool.IcingaRESTApi `
-Command 'New-IcingaForWindowsRESTApi' `
-CmdParameters @{
'IcingaDaemonData' = $global:IcingaDaemonData;
'Port' = $Port;
'RootFolder' = $RootFolder;
'CertFile' = $CertFile;
'CertThumbprint' = $CertThumbprint;
'RequireAuth' = $RequireAuth;
} `
-Start;
$ThreadId = 0;
while ($ConcurrentThreads -gt 0) {
$ConcurrentThreads = $ConcurrentThreads - 1;
[System.Collections.Queue]$RESTThreadQueue = @();
$global:IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.Add($ThreadId, [System.Collections.Queue]::Synchronized($RESTThreadQueue));
Start-IcingaForWindowsRESTThread -ThreadId $ThreadId -RequireAuth:$RequireAuth;
$ThreadId += 1;
Start-Sleep -Seconds 1;
}
}

View file

@ -0,0 +1,49 @@
function Register-IcingaEventLogMessagesRESTApi()
{
return @{
'RESTApi' = @{
2000 = @{
'EntryType' = 'Error';
'Message' = 'Failed to start REST-Api daemon, as no valid provided SSL and Icinga 2 Agent certificate was found';
'Details' = 'While starting the Icinga for Windows REST-Api daemon, no valid certificate was found for usage. You can either share a valid certificate by defining the full path with `-CertFile` to a .crt, .cert or .pfx file, by using `-CertThumbprint` to lookup a certificate inside the Microsoft cert store and by default the Icinga 2 Agent certificates. Please note that only Icinga 2 Agent version 2.8.0 or later are supported';
'EventId' = 2000;
};
2001 = @{
'EntryType' = 'Error';
'Message' = 'Failed to start REST-Api daemon in JEA context';
'Details' = 'Icinga for Windows is being used inside a JEA context as service with the REST-Api daemon. To establish a secure TLS socket, it is required to create certificates in advance for the socket to bind on with "Install-IcingaForWindowsCertificate". The REST-Api daemon will now exit.';
'EventId' = 2001;
};
2050 = @{
'EntryType' = 'Error';
'Message' = 'Failed to parse received REST-Api call';
'Details' = 'An API call send to the daemon could not be processed and caused an exception. Further details about the cause of this error can be found below.';
'EventId' = 2050;
};
2051 = @{
'EntryType' = 'Error';
'Message' = 'Failed to execute API call';
'Details' = 'An API call could not be processed due to an internal exception.';
'EventId' = 2051;
};
2052 = @{
'EntryType' = 'Error';
'Message' = 'Internal exception on calling API command';
'Details' = 'An internal command assigned to an API request could not be executed and caused an exception.';
'EventId' = 2052;
};
2100 = @{
'EntryType' = 'Warning';
'Message' = 'Failed to add namespace configuration for executed commands, as previous commands are reporting identical namespace identifiers';
'Details' = 'This warning occurs while the REST-Api is trying to auto-load different resources automatically to provide for example inventory information or any other auto-loaded configurations. Please review your installed modules, check the detailed description which modules and Cmdlets caused this conflict and either resolve it or get in contact with the corresponding developers.';
'EventId' = 2100;
};
2101 = @{
'EntryType' = 'Warning';
'Message' = 'Failed to add namespace configuration for command aliases, as an identical namespace was already added';
'Details' = 'This warning occurs while the REST-Api is trying to auto-load different resources automatically to provide for example command aliases for providing inventory information or any other auto-loaded configurations. Please review your installed modules, check the detailed description which modules and Cmdlets caused this conflict and either resolve it or get in contact with the corresponding developers.';
'EventId' = 2101;
};
}
};
}

View file

@ -0,0 +1,13 @@
function Get-IcingaNextRESTApiThreadId()
{
[int]$ConcurrentThreads = $IcingaDaemonData.IcingaThreadContent.RESTApi.TotalThreads - 1;
[int]$LastThreadId = $IcingaDaemonData.IcingaThreadContent.RESTApi.LastThreadId + 1;
if ($LastThreadId -gt $ConcurrentThreads) {
$LastThreadId = 0;
}
$IcingaDaemonData.IcingaThreadContent.RESTApi.LastThreadId = $LastThreadId;
return $LastThreadId;
}

View file

@ -0,0 +1,118 @@
function New-IcingaForWindowsRESTThread()
{
param(
$IcingaDaemonData,
$RequireAuth,
$ThreadId
);
# Import the framework library components and initialise it
# as daemon
Use-Icinga -LibOnly -Daemon;
$Global:IcingaDaemonData = $IcingaDaemonData;
# Initialise our performance counter categories
Show-IcingaPerformanceCounterCategories | Out-Null;
while ($TRUE) {
try {
if ($IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.ContainsKey($ThreadId) -eq $FALSE) {
Start-Sleep -Milliseconds 10;
continue;
}
if ($IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.$ThreadId.Count -eq 0) {
Start-Sleep -Milliseconds 10;
continue;
}
$Connection = $IcingaDaemonData.IcingaThreadContent.RESTApi.ApiRequests.$ThreadId.Dequeue();
if ($null -eq $Connection) {
Start-Sleep -Milliseconds 10;
continue;
}
# Read the received message from the stream by using our smart functions
[string]$RestMessage = Read-IcingaTCPStream -Client $Connection.Client -Stream $Connection.Stream;
# Now properly translate the entire rest message to a parseable hashtable
$RESTRequest = Read-IcingaRestMessage -RestMessage $RestMessage -Connection $Connection;
if ($null -ne $RESTRequest) {
# Check if we require to authenticate the user
if ($RequireAuth) {
# If no authentication header is provided we should show the prompt
if ([string]::IsNullOrEmpty($RESTRequest.Header.Authorization)) {
# In case we do not send an authentication header increase the blacklist counter
# to ensure we are not spammed and "attacked" by a client with useless requests
Add-IcingaRESTClientBlacklistCount `
-Client $Connection.Client `
-ClientList $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi.ClientBlacklist;
# Send the authentication prompt
Send-IcingaWebAuthMessage -Connection $Connection;
# Close the connection
Close-IcingaTCPConnection -Client $Connection.Client;
continue;
}
$Credentials = Convert-Base64ToCredentials -AuthString $RESTRequest.Header.Authorization;
[bool]$LoginSuccess = Test-IcingaRESTCredentials -UserName $Credentials.user -Password $Credentials.password -Domain $Credentials.domain;
$Credentials = $null;
# Handle login failures
if ($LoginSuccess -eq $FALSE) {
# Failed attempts should increase the blacklist counter
Add-IcingaRESTClientBlacklistCount `
-Client $Connection.Client `
-ClientList $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi.ClientBlacklist;
# Re-send the authentication prompt
Send-IcingaWebAuthMessage -Connection $Connection;
# Close the connection
Close-IcingaTCPConnection -Client $Connection.Client;
continue;
}
}
# We should remove clients from the blacklist who are sending valid requests
Remove-IcingaRESTClientBlacklist -Client $Connection.Client -ClientList $IcingaDaemonData.BackgroundDaemon.IcingaPowerShellRestApi.ClientBlacklist;
switch (Get-IcingaRESTPathElement -Request $RESTRequest -Index 0) {
'v1' {
Invoke-IcingaRESTAPIv1Calls -Request $RESTRequest -Connection $Connection;
break;
};
default {
Write-IcingaDebugMessage -Message ('Invalid API call - no version specified' + ($RESTRequest.RequestPath | Out-String));
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Not Found') `
-ContentBody 'Invalid API call received. No version specified.'
) -Stream $Connection.Stream;
};
}
}
} catch {
$ExMsg = $_.Exception.Message;
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Internal Server Error') `
-ContentBody $ExMsg
) -Stream $Connection.Stream;
Write-IcingaEventMessage -Namespace 'RESTApi' -EventId 2051 -Objects $ExMsg;
}
# Finally close the clients connection as we are done here and
# ensure this thread will close by simply leaving the function
if ($null -ne $Connection) {
Close-IcingaTCPConnection -Client $Connection.Client;
}
# Cleanup the error stack and remove not required data
$Error.Clear();
# Force PowerShell to call the garbage collector to free memory
[System.GC]::Collect();
}
}

View file

@ -0,0 +1,20 @@
function Start-IcingaForWindowsRESTThread()
{
param (
[int]$ThreadId = 0,
[switch]$RequireAuth = $FALSE
);
# Now create a new thread, assign a name and parse all required arguments to it.
# Last but not least start it directly
New-IcingaThreadInstance `
-Name ([string]::Format("Icinga_Windows_REST_Api_Thread_{0}", $ThreadId)) `
-ThreadPool (New-IcingaThreadPool -MaxInstances 1) `
-Command 'New-IcingaForWindowsRESTThread' `
-CmdParameters @{
'IcingaDaemonData' = $global:IcingaDaemonData;
'RequireAuth' = $RequireAuth;
'ThreadId' = $ThreadId;
} `
-Start;
}

View file

@ -0,0 +1,53 @@
<#
.SYNOPSIS
Adds a Cmdlet to an REST-Api endpoint which is either whitelisted or blacklisted.
Whitelisted Cmdlets can be executed over API endpoints, blacklisted not.
Use '*' for wildcard matches
.DESCRIPTION
Adds a Cmdlet to an REST-Api endpoint which is either whitelisted or blacklisted.
Whitelisted Cmdlets can be executed over API endpoints, blacklisted not.
Use '*' for wildcard matches
.FUNCTIONALITY
Enables or disables Cmdlets for REST-Api endpoints
.EXAMPLE
PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker';
.EXAMPLE
PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker' -Blacklist;
.LINK
https://github.com/Icinga/icinga-powershell-restapi
#>
function Add-IcingaRESTApiCommand()
{
param (
[string]$Command = '',
[string]$Endpoint = '',
[switch]$Blacklist = $FALSE
);
if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) {
return;
}
$Commands = $null;
$ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint));
[array]$Values = @();
if ($Blacklist) {
$ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint));
}
$Commands = Get-IcingaPowerShellConfig -Path $ConfigPath;
if ((Test-IcingaPowerShellConfigItem -ConfigObject $Commands -ConfigKey $Command)) {
return;
}
if ($null -ne $Commands) {
$Values = $Commands;
}
$Values += $Command;
Set-IcingaPowerShellConfig -Path $ConfigPath -Value $Values;
}

View file

@ -0,0 +1,49 @@
<#
.SYNOPSIS
Removes a Cmdlet from an REST-Api endpoints whitelist or blacklist.
.DESCRIPTION
Removes a Cmdlet from an REST-Api endpoints whitelist or blacklist.
.FUNCTIONALITY
Removes Cmdlets for REST-Api endpoints
.EXAMPLE
PS>Remove-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker';
.EXAMPLE
PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker' -Blacklist;
.LINK
https://github.com/Icinga/icinga-powershell-restapi
#>
function Remove-IcingaRESTApiCommand()
{
param (
[string]$Command = '',
[string]$Endpoint = '',
[switch]$Blacklist = $FALSE
);
if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) {
return;
}
$Commands = $null;
$ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint));
[array]$Values = @();
if ($Blacklist) {
$ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint));
}
$Commands = Get-IcingaPowerShellConfig -Path $ConfigPath;
if ($null -eq $Commands) {
return;
}
foreach ($element in $Commands) {
if ($element.ToLower() -ne $Command.ToLower()) {
$Values += $element;
}
}
Set-IcingaPowerShellConfig -Path $ConfigPath -Value $Values;
}

View file

@ -0,0 +1,44 @@
<#
.SYNOPSIS
Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist.
This function will return True if the command is whitelisted and False if it is
blacklisted. If the Cmdlet is not added anywhere, the function will return False as well.
.DESCRIPTION
Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist.
This function will return True if the command is whitelisted and False if it is
blacklisted. If the Cmdlet is not added anywhere, the function will return False as well.
.FUNCTIONALITY
Tests if a Cmdlet is allowed to be executed over the REST-Api
.EXAMPLE
PS>Test-IcingaRESTApiCommand -Command 'Invoke-IcingaCheckCPU' -Endpoint 'checker';
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>
function Show-IcingaRESTApiCommands()
{
$Commands = Get-IcingaPowerShellConfig -Path 'RESTApi.Commands';
foreach ($property in $Commands.PSObject.Properties) {
$Whitelisted = 'None';
$Blacklisted = 'None';
if (Test-IcingaPowerShellConfigItem -ConfigObject $property.Value -ConfigKey 'Whitelist') {
if ($property.Value.Whitelist.Count -ne 0) {
$Whitelisted = [string]::Join(', ', ($property.Value.Whitelist));
}
}
if (Test-IcingaPowerShellConfigItem -ConfigObject $property.Value -ConfigKey 'Blacklist') {
if ($property.Value.Blacklist.Count -ne 0) {
$Blacklisted = [string]::Join(', ', ($property.Value.Blacklist));
}
}
Write-IcingaConsolePlain -Message 'API Endpoint "{0}"' -Objects $property.Name;
Write-IcingaConsolePlain -Message '################';
Write-IcingaConsolePlain -Message '';
Write-IcingaConsolePlain -Message 'Whitelisted: {0}' -Objects $Whitelisted;
Write-IcingaConsolePlain -Message '';
Write-IcingaConsolePlain -Message 'Blacklisted: {0}' -Objects $Blacklisted;
Write-IcingaConsolePlain -Message '';
}
}

View file

@ -0,0 +1,46 @@
<#
.SYNOPSIS
Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist.
This function will return True if the command is whitelisted and False if it is
blacklisted. If the Cmdlet is not added anywhere, the function will return False as well.
.DESCRIPTION
Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist.
This function will return True if the command is whitelisted and False if it is
blacklisted. If the Cmdlet is not added anywhere, the function will return False as well.
.FUNCTIONALITY
Tests if a Cmdlet is allowed to be executed over the REST-Api
.EXAMPLE
PS>Test-IcingaRESTApiCommand -Command 'Invoke-IcingaCheckCPU' -Endpoint 'checker';
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>
function Test-IcingaRESTApiCommand()
{
param (
[string]$Command = '',
[string]$Endpoint = ''
);
if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) {
return $FALSE;
}
$WhiteList = Get-IcingaPowerShellConfig -Path ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint));
$Blacklist = Get-IcingaPowerShellConfig -Path ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint));
foreach ($entry in $Blacklist) {
if ($Command.ToLower() -like $entry.ToLower()) {
return $FALSE;
}
}
foreach ($entry in $WhiteList) {
if ($Command.ToLower() -like $entry.ToLower()) {
return $TRUE;
}
}
# If the command is not configured, always return false
return $FALSE;
}

View file

@ -0,0 +1,147 @@
function Invoke-IcingaApiChecksRESTCall()
{
param (
[Hashtable]$Request = @{ },
[Hashtable]$Connection = @{ },
$IcingaGlobals,
[string]$ApiVersion = $null
);
$Global:IcingaDaemonData = $IcingaGlobals;
# Initialise some global variables we use to actually store check result data from
# plugins properly. This is doable from each thread instance as this part isn't
# shared between daemons
New-IcingaCheckSchedulerEnvironment;
[Hashtable]$ContentResponse = @{ };
# Short our call
$CheckerAliases = $IcingaGlobals.BackgroundDaemon.IcingaPowerShellRestApi.CommandAliases.checker;
$CheckConfig = $Request.Body;
[int]$ExitCode = 3; #Unknown
# Check if there are an inventory aliases configured
# This should be maintained by the developer and not occur
# anyway
if ($null -eq $CheckerAliases) {
$CheckerAliases = @{ };
}
if ((Get-IcingaRESTHeaderValue -Request $Request -Header 'Content-Type') -ne 'application/json' -And $Request.Method -eq 'POST') {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Bad Request') `
-ContentBody 'This API endpoint does only accept "application/json" as content type over POST.'
) -Stream $Connection.Stream;
return;
}
# Our namespace to include inventory packages is 'include' over the api
# Everything else will be dropped for the moment
if ($Request.RequestArguments.ContainsKey('list')) {
Add-IcingaHashtableItem `
-Hashtable $ContentResponse `
-Key 'Commands' `
-Value $CheckerAliases | Out-Null;
} elseif ($Request.RequestArguments.ContainsKey('command')) {
[string]$ExecuteCommand = $null;
foreach ($element in $CheckerAliases.Keys) {
if ($Request.RequestArguments.command -Contains $element) {
$ExecuteCommand = $CheckerAliases[$element];
# We only support to execute one check per call
# No need to loop through everything
break;
}
}
if ([string]::IsNullOrEmpty($ExecuteCommand)) {
[string]$ExecuteCommand = $Request.RequestArguments.command;
}
if ((Test-IcingaRESTApiCommand -Command $ExecuteCommand -Endpoint 'apichecks') -eq $FALSE) {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Forbidden') `
-ContentBody ([string]::Format('The command "{0}" you are trying to execute over this REST-Api endpoint "apichecks" is not whitelisted for remote execution.', $ExecuteCommand))
) -Stream $Connection.Stream;
return;
}
Write-IcingaDebugMessage -Message ('Executing API check for command: ' + $ExecuteCommand);
if ([string]::IsNullOrEmpty($CheckConfig) -eq $FALSE -And $Request.Method -eq 'POST') {
# Convert our JSON config for checks to a PSCustomObject
$PSArguments = ConvertFrom-Json -InputObject $CheckConfig;
# For executing the checks, we will require the data as
# hashtable, so declare it here
[hashtable]$Arguments = @{ };
# Now convert our custom object by Key<->Value to
# a valid hashtable, allowing us to parse arguments
# to our check command
$PSArguments.PSObject.Properties | ForEach-Object {
Add-IcingaHashtableItem `
-Hashtable $Arguments `
-Key $_.Name `
-Value $_.Value | Out-Null;
};
[int]$ExitCode = & $ExecuteCommand @Arguments;
} elseif ($Request.Method -eq 'GET') {
[int]$ExitCode = & $ExecuteCommand;
} else {
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) `
-ContentBody @{ 'message' = 'This API endpoint does only accept GET and POST methods for requests.' }
) -Stream $Connection.Stream;
return;
}
# Once the check is executed, the plugin output and the performance data are stored
# within a special cache map we can use for accessing
$CheckResult = Get-IcingaCheckSchedulerPluginOutput;
[array]$PerfData = Get-IcingaCheckSchedulerPerfData;
# Ensure our PerfData variable is always an array
if ($null -eq $PerfData -Or $PerfData.Count -eq 0) {
[array]$PerfData = @();
}
# Free our memory again
Clear-IcingaCheckSchedulerEnvironment;
Write-IcingaDebugMessage -Message 'Check Executed. Result below' -Objects $ExecuteCommand, $CheckResult, $PerfData, $ExitCode;
Add-IcingaHashtableItem `
-Hashtable $ContentResponse `
-Key $ExecuteCommand `
-Value @{
'exitcode' = $ExitCode;
'checkresult' = $CheckResult;
'perfdata' = $PerfData;
} | Out-Null;
}
if ($ContentResponse.Count -eq 0) {
$ContentResponse.Add(
'message',
'Welcome to the Icinga for Windows API checker. To execute checks, please use the command parameter. For providing arguments, you will have to submit a post with JSON encoded arguments. Example: /v1/checker?command=Invoke-IcingaCheckCPU'
);
}
# Send the response to the client
Send-IcingaTCPClientMessage -Message (
New-IcingaTCPClientRESTMessage `
-HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) `
-ContentBody $ContentResponse
) -Stream $Connection.Stream;
}

View file

@ -0,0 +1,74 @@
<#
.SYNOPSIS
Registers an endpoint called 'checker' in our REST-Api
.DESCRIPTION
This module will provide an endpoint for the Icinga for Windows REST-Api
https://github.com/Icinga/icinga-powershell-restapi
The endpoint itself is called 'checker' and will map available Cmdlets for
check plugins
.FUNCTIONALITY
This module will depend on the Icinga Plugins as well as the Framework and
the REST-Api. It will provide a wrapper to execute Icinga Plugins over the
REST-Api and fetch the plugin output
.OUTPUTS
$NULL
.LINK
https://github.com/Icinga/icinga-powershell-apichecks
.NOTES
#>
function Register-IcingaRESTAPIEndpointAPIChecks()
{
return @{
'Alias' = 'checker';
'Command' = 'Invoke-IcingaApiChecksRESTCall';
};
}
<#
.SYNOPSIS
Registers our command aliases for mapping the 'command' argument
from our REST-Call securely to our PowerShell Cmdlets
.DESCRIPTION
This module will provide an endpoint for the Icinga for Windows REST-Api
https://github.com/Icinga/icinga-powershell-restapi
By using the references, we can register endpoints and aliases to fetch
informations
.EXAMPLE
https://example.com/v1/checker?command=cpu
.FUNCTIONALITY
This module will depend on the Icinga Plugins as well as the Framework and
the REST-Api. It will provide a wrapper to execute Icinga Plugins over the
REST-Api and fetch the plugin output
.OUTPUTS
$NULL
.LINK
https://github.com/Icinga/icinga-powershell-apichecks
.NOTES
#>
function Register-IcingaRESTApiCommandAliasesApiChecks()
{
[array]$CmdList = Get-Command -Name 'Invoke-IcingaCheck*';
[hashtable]$CmdAlias = @{
'checker' = @{ }
};
$FrameworkRoot = Get-IcingaForWindowsRootPath;
foreach ($entry in $CmdList) {
# Only include checks being installed on the same location as our Framework
if ($entry.Module.Path.Contains($FrameworkRoot) -eq $FALSE) {
continue;
}
$CmdAliasName = $entry.Name.Replace('Invoke-IcingaCheck', '');
if ($CmdAlias.checker.ContainsKey($CmdAliasName)) {
continue;
}
$CmdAlias.checker.Add($CmdAliasName, $Entry.Name);
}
return $CmdAlias;
}