Skip to content

Developer Guide: Custom API-Endpoints

Starting with Icinga PowerShell Framework v1.1.0 plenty of features and functionality have been added for shipping data by using a REST-API. This Developer Guide will describe on how to write custom API endpoints by using the PowerShell Framework v1.1.0 and the Icinga PowerShell REST-Api. In this example we will write a custom endpoint to simply provide a file list for a specific folder.

File Structure

Like plugins, API endpoints can contain plenty of different files to keep the code clean. To ensure each module is identical and easier to maintain for users, we would advise the following file structure:

module
  |_ apiendpoint.psd1
  |_ apiendpoint.psm1
  |_ lib
     |_ function1.psm1
     |_ function2.psm1

This will ensure these functions can be called separately from the endpoint itself and make re-using them a lot easier. In addition, it will help other developers to build dependencies based on your module and allow an easier re-usage of already existing components.

Additional required files within the lib folder can be included by using the NestedModules array within your psd1 file. This will ensure these files are automatically loaded once a new PowerShell session is started.

Creating A New Module

The best approach for creating a custom API endpoint is by creating an independent module which is installed in your PowerShell modules directly. This will ensure you are not overwriting your custom data with possible other module updates.

Developer Tools

To get started easier, you can run this command to create the new module:

New-IcingaForWindowsComponent -Name 'apitutorial' -ComponentType 'apiendpoint';

If you wish to create the module manually, please read on.

Manual Creation

In this guide, we will assume the name of the module is icinga-powershell-apitutorial.

At first we will have to create a new module. Navigate to the PowerShell modules folder the Framework itself is installed to. In this tutorial we will assume the location is

C:\Program Files\WindowsPowerShell\Modules

Now create a new folder with the name icinga-powershell-apitutorial and navigate into it.

As we require a psm1 file which contains our code, we will create a new file with the name icinga-powershell-apitutorial.psm1. This will allow the PowerShell autoloader to load the module automatically.

Note: It could be possible, depending on your execution policies, that your module is not loaded properly. If this is the case, you can try to unblock the file by opening a PowerShell and use the Unblock-File Cmdlet

Unblock-File -Path 'C:\Program Files\WindowsPowerShell\Modules\icinga-powershell-apitutorial\icinga-powershell-apitutorial.psm1'

Testing The Module

Once the module files are created and unblocked, we can start testing if the autoloader is properly working and our module is detected.

For this open the file icinga-powershell-apitutorial.psm1 in your preferred editor and add the following code snippet

function Test-MyIcingaAPITutorialCommand()
{
    Write-Host 'Module was loaded';
}

Now open a new PowerShell terminal or write powershell into an already open PowerShell prompt and execute the command Test-MyIcingaAPITutorialCommand.

If everything went properly, you should now read the output Module was loaded in our prompt. If not, you can try to import the module by using

Import-Module 'C:\Program Files\WindowsPowerShell\Modules\icinga-powershell-apitutorial\icinga-powershell-apitutorial.psm1';

inside your console prompt. After that try again to execute the command Test-MyIcingaAPITutorialCommand and check if it works this time. If not, you might check the naming of your module to ensure folder name and .psm1 file name is identical.

Once this is working, we can remove the function again as we no longer require it.

Create A New API-Endpoint

Once everything is working properly we can create our starting function we later use to execute our API endpoint.

At first we create a new folder lib inside our module folder and inside the file Invoke-IcingaAPITutorialRESTCall.psm1. For naming guidelines we will have to use Invoke-Icinga{0}RESTCall. Replace {0} with a unique name describing shortly what your module is doing. The user will not require to use this function later and is only required internally and to have a better look on which function is providing REST endpoints.

So lets get started with the function

function Invoke-IcingaAPITutorialRESTCall()
{
    # Our code belongs here
}

Basic API Architecture

A developer using the REST-Api integration does not have to worry about anything regarding header fetching, URL encoding or similar. All data is parsed by the Icinga PowerShell REST-Api and invoked to our function.

Our API endpoint will be called by a namespace, referring to our actual function executing the code.

Writing Our Base-Skeleton

For our API endpoint we will start with param() to parse arguments to our endpoint which is standardized, and has to be followed. Otherwise the integration might not work.

function Invoke-IcingaAPITutorialRESTCall()
{
    # Create our arguments the REST-Api daemon will use to parse the request
    param (
        [Hashtable]$Request    = @{ },
        [Hashtable]$Connection = @{ },
        [string]$ApiVersion    = $null
    );
}

Request Argument

The request argument provides a hashtable with all parsed content of the request to later work with. The following elements are available by default:

Method

The HTTP method being used for the request, like GET, POST, DELETE and so on

RequestPath

The request path is split into two hashtable entries: FullPath and PathArray. This tells you exactly which URL the user specified and allows you to build proper handling for different entry points of your endpoint.

For the path array, on index 0 you will always find the version and on index 1 your endpoint alias. Following this, possible additional path extensions in your module will always start on index 2.

A hashtable containing all send headers by the client. If you require your client to send additional headers for certain tasks to work, you can check with this if the header is set with the correct value.

Name                           Value
----                           -----
Upgrade-Insecure-Requests      1
User-Agent                     Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept                         text/html,application/json
Host                           example.com:5668
Sec-Fetch-Dest                 document
Accept-Language                de,en-US;q=0.9,en;q=0.8
Connection                     keep-alive
Accept-Encoding                gzip, deflate, br
Sec-Fetch-Mode                 navigate
sec-ch-ua-mobile               ?0
X-CustomHeader                 Custom Content
RequestArguments

Of course we will also handle possible request arguments. This could either be used for filtering or to modify returned content depending on the input. An example could look like this:

https://example.com:5668/v1/apitutorial?include=*psm1&exclude=*api*
Name                           Value
----                           -----
include                        {*psm1}
exclude                        {*api*}
Body

The content send by the client in case a method is used to send data.

Note: The body argument is only available in case data is send. If the client is using POST and sending no data, the argument is not present.

FullRequest

This argument contains the full request string for possible troubleshooting and debugging.

/v1/apitutorial?include=*psm1&exclude=*api*
ContentLength

This only applies to any request which can send data as body and tells you how many data was send. This part is moved from the header to this location for easier accessing.

Connection Argument

This argument is containing the connection details of the client including the TCP stream object. You only require this for sending data back to the client or for troubleshooting. In general you only have to parse this object to other functions without modifying it.

Sending Data to the Client

Now we are basically ready to process data. To do so, we will fetch the current folder content of our PowerShell module with Get-ChildItem and send this content to our client. For sending data to the client, we can use Send-IcingaTCPClientMessage. This Cmdlet will use a Message as New-IcingaTCPClientRESTMessage object which itself contains the HTTPResponse and our ContentBody. In addition to Send-IcingaTCPClientMessage we also have to specify the Stream to write to. The stream object is part of our Connection argument.

All content will be send as JSON encoded, so please ensure you are using a datatype which is convertible by ConvertTo-Json.

function Invoke-IcingaAPITutorialRESTCall()
{
    # Create our arguments the REST-Api daemon will use to parse the request
    param (
        [Hashtable]$Request    = @{ },
        [Hashtable]$Connection = @{ },
        [string]$ApiVersion    = $null
    );

    # Fetch all file names within our module directory. We filter this to ensure we
    # do not have to handle all PSObjects, we our client message functionality will
    # try to resolve them. This could end up in an almost infinite loop
    $Content = Get-ChildItem -Path 'C:\Program Files\WindowsPowerShell\Modules\' -Recurse | Select-Object 'Name', 'FullName';

    # Send the response to the client as 200 "Ok" with out Directory body
    Send-IcingaTCPClientMessage -Message (
        New-IcingaTCPClientRESTMessage `
            -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) `
            -ContentBody $Content
    ) -Stream $Connection.Stream;
}

Registering API-Endpoints

Now as we have written a basic function to fetch folder content and to send it back to our client, we will have to register our Cmdlet to the endpoint. For this we will open our icinga-powershell-apitutorial.psm1 and add a namespace function which has to follow this naming guideline: Register-IcingaRESTAPIEndpoint{0}

Replace {0} with the name you have chosen for your Invoke-Icinga{0}RESTCall. Once the REST-Api Daemon is loaded, all functions within this namespace are executed. The function has to return a hashtable with an Alias referring to the URL part the user has to enter and a Command being executed for this alias.

function Register-IcingaRESTAPIEndpointAPITutorial()
{
    return @{
        'Alias'   = 'apitutorial';
        'Command' = 'Invoke-IcingaAPITutorialRESTCall';
    };
}

If our module is providing different endpoints, you will have to create multiple register functions. To keep the API how ever clean and prevent conflicting, we advice you to provide only one endpoint and handle all other tasks within this endpoint.

As everything is now ready, we can restart our Icinga PowerShell Framework service by using

Restart-IcingaForWindows;

and access our API endpoint by browsing to our API location (in our example we assume you use 5668 as default port):

https://example.com:5668/v1/apitutorial
[
    {
        "Name": "icinga-powershell-apitutorial",
        "FullName": "C:\\Program Files\\WindowsPowerShell\\Modules\\icinga-powershell-apitutorial"
    },
    {
        "Name": "icinga-powershell-framework",
        "FullName": "C:\\Program Files\\WindowsPowerShell\\Modules\\icinga-powershell-framework"
    },
    {
        "Name": "icinga-powershell-inventory",
        "FullName": "C:\\Program Files\\WindowsPowerShell\\Modules\\icinga-powershell-inventory"
    },
    {
        "Name": "icinga-powershell-plugins",
        "FullName": "C:\\Program Files\\WindowsPowerShell\\Modules\\icinga-powershell-plugins"
    },
    {
        "Name": "icinga-powershell-restapi",
        "FullName": "C:\\Program Files\\WindowsPowerShell\\Modules\\icinga-powershell-restapi"
    },
    ...
]

Conclusion

This is a basic tutorial on how to write custom API-Endpoints and make them available in your environment. Of course you can now start to filter requests depending on the URL the user added, used headers or other input like the body for example. All data send by the client is accessible to developers for writing their own extensions and modules.