Table of Contents

Creating an API to retrieve elements in alarm

Important

We highly recommend following the hello world tutorial before you start this tutorial, as it builds on the knowledge of that first tutorial.

In this tutorial, you will build a more realistic use case for an API. This API will allow you to retrieve a list of elements that have alarms with a specified alarm level. It will process and validate the input, get all the elements according to this input, and return them as a JSON response.

The content and screenshots for this tutorial were created in DataMiner version 10.3.9.

Overview

Step 1: Create the Automation script

To develop this new API script, you can use the Automation script solution from the hello world tutorial. Make sure you have this solution open in Visual Studio. You will need a new script project to develop this new API.

  1. In Visual Studio, right-click the solution at the top of the Solution Explorer, and select Add > New DataMiner Automation Script.

    Visual Studio add new Automation script solution

  2. In the dialog, enter the name "ElementsAPI".

    Create new automation script solution

The new Automation project has now been created for this API script. In the next sections, you will expand this script step by step:

  1. Prepare the script
  2. Store user input
  3. Validate user input
  4. Expand the user input with JSON
  5. Retrieve the element data
  6. Publish the script

Prepare the script

Before the API logic can be written, the API script wrapper code needs to be present. The default run method should be replaced with a special entry point method.

  1. Double-click the ElementsAPI_1.cs file in the Solution Explorer.

    Visual Studio Automation Script

  2. In the code window, remove the default Run(IEngine engine) method.

  3. Right-click the empty line between the Script class brackets and select Snippet > Insert Snippet.

    Insert snippet

  4. Select the snippet DIS > Automation Script > CreateUserDefinedAPI.

    Add entrypoint

The script has now been updated with the OnApiTrigger(IEngine, ApiTriggerInput) method. You can also remove the comments and unused "using" directives. This should result in the following script code:

namespace ElementsAPI_1
{
    using Skyline.DataMiner.Automation;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis.Actions;

    public class Script
    {

        [AutomationEntryPoint(AutomationEntryPointType.Types.OnApiTrigger)]
        public ApiTriggerOutput OnApiTrigger(IEngine engine, ApiTriggerInput requestData)
        {
            return new ApiTriggerOutput
            {
                ResponseBody = "Succeeded",
                ResponseCode = (int)StatusCode.Ok,
            };
        }
    }
}

Store user input

For this API, the input from the user should be the alarm level (minor, major, etc.) for which the user wants to see the elements. Anything passed in the body of the API request will be available as a string in the RawBody property of the requestData input argument of the entrypoint method. This content of the rawBody can be stored in a variable.

namespace ElementsAPI_1
{
    using Skyline.DataMiner.Automation;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis.Actions;

    public class Script
    {
        [AutomationEntryPoint(AutomationEntryPointType.Types.OnApiTrigger)]
        public ApiTriggerOutput OnApiTrigger(IEngine engine, ApiTriggerInput requestData)
        {
            var alarmLevel = requestData.RawBody;
            
            return new ApiTriggerOutput
            {
                ResponseBody = "Succeeded",
                ResponseCode = (int)StatusCode.Ok,
            };
        }
    }
}

Validate user input

The API will currently accept any string as input, even if this is not a valid alarm level. This is why there should be a validation step.

You can add a collection of valid alarm levels, which will be used to check the input. If no valid match is found, a clear error can be returned.

For the response code, the integer representation of the StatusCode.BadRequest enum option can be assigned. This will cause the response of the API to be clearly marked as a user request error with the appropriate response code (400).

namespace ElementsAPI_1
{
    using System.Collections.Generic;
    using Skyline.DataMiner.Automation;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis.Actions;

    public class Script
    {
        private readonly List<string> _knownAlarmLevels
            = new List<string> { "Warning", "Minor", "Major", "Critical" };

        [AutomationEntryPoint(AutomationEntryPointType.Types.OnApiTrigger)]
        public ApiTriggerOutput OnApiTrigger(IEngine engine, ApiTriggerInput requestData)
        {
            var alarmLevel = requestData.RawBody;

            if (string.IsNullOrWhiteSpace(alarmLevel) || !_knownAlarmLevels.Contains(alarmLevel))
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody =
                        $"Invalid alarm level passed, possible values are: ${string.Join(",", _knownAlarmLevels)}",
                    ResponseCode = (int)StatusCode.BadRequest,
                };
            }

            return new ApiTriggerOutput
            {
                ResponseBody = "Succeeded",
                ResponseCode = (int)StatusCode.Ok,
            };
        }
    }
}

Expand the user input with JSON

On large systems, this API could potentially return a large number of records. To prevent performance issues, this should be limited to a certain number, which can also be provided by the user.

The current version of the script expects only one single string value from the body, but now two will have to be sent. This is where JSON can be used to structure the input data and allow you to make APIs that expect multiple input values.

To use JSON, you will need to include the Newtonsoft.Json NuGet package.

  1. In Visual Studio, right-click the project ElementsAPI_1 and click Manage NuGet packages.

    Visual Studio project dependencies

  2. In the window that opens, click Browse at the top.

    Visual Studio NuGet browse

  3. Now search for "Newtonsoft.Json", and click the first package.

  4. On the right, click Install.

    Install Newtonsoft.Json

Note

You could also use the built-in parameter conversion instead of doing the JSON deserialization within this script. This could be a simpler approach if you are less familiar with it. To find out more, see User input data.

To convert (i.e. deserialize) the input JSON to data that can be used in the script, you will have to define a class that contains all the input data that is expected. In this case, the input would look like this:

{
    "alarmLevel" : "Minor",
    "limit" : 10
}

The C# class should then look like this:

public class Input
{
    public string AlarmLevel { get; set; }

    public int Limit { get; set; }
}

When this is done, you can add the actual conversion. By calling JsonConvert.DeserializeObject<Input>(), you can easily do so. The library will parse the input and return an instance of the input class where the values have been mapped to the values provided in that JSON input.

When the input data is not correctly formatted, exceptions can be thrown. This is why a try-catch statements should be placed around this conversion. When an exception occurs, a clear error can be returned explaining this. A check can be added at the start of the script that returns an error when the body is empty. This will reduce the number of scenarios where things can go wrong.

Note that there are also JsonSerializerSettings defined. These ensure that the conversion will always accept and output camel-case JSON names, which is the recommended casing type for JSON. ("alarmLevel" instead of "AlarmLevel")

namespace ElementsAPI_1
{
    using System.Collections.Generic;
    using Models;
    using Skyline.DataMiner.Automation;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis.Actions;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;

    public class Script
    {
        private readonly List<string> _knownAlarmLevels
            = new List<string> { "Warning", "Minor", "Major", "Critical" };

        private readonly JsonSerializerSettings _serializerSettings 
            = new JsonSerializerSettings()
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
            };

        [AutomationEntryPoint(AutomationEntryPointType.Types.OnApiTrigger)]
        public ApiTriggerOutput OnApiTrigger(IEngine engine, ApiTriggerInput requestData)
        {
            if (string.IsNullOrWhiteSpace(requestData.RawBody))
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody = "Request body cannot be empty",
                    ResponseCode = (int)StatusCode.BadRequest,
                };
            }

            Input input;
            try
            {
                input = JsonConvert.DeserializeObject<Input>(
                    requestData.RawBody ?? string.Empty, _serializerSettings);
            }
            catch
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody = "Could not parse request body.",
                    ResponseCode = (int)StatusCode.InternalServerError,
                };
            }

            if (string.IsNullOrWhiteSpace(input.AlarmLevel) || !_knownAlarmLevels.Contains(input.AlarmLevel))
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody =
                        $"Invalid alarm level passed, possible values are: ${string.Join(",", _knownAlarmLevels)}",
                    ResponseCode = (int)StatusCode.BadRequest,
                };
            }

            return new ApiTriggerOutput
            {
                ResponseBody = "Succeeded",
                ResponseCode = (int)StatusCode.Ok,
            };
        }
    }
}

namespace Models
{
    public class Input
    {
        public string AlarmLevel { get; set; }

        public int Limit { get; set; }
    }
}

Retrieve the element data

After the input data is converted and validated, it can be used to retrieve the actual element data. This logic should preferably be put into a separate method called GetElements(). This method:

  • Accepts the input data.
  • Converts it to a filter.
  • Requests the elements from DataMiner via IEngine.
  • Converts the element data to a specific ElementInfo output class.
  • Ensures the number of records does not exceed the limit set by the input.

Just like with the input data conversion, exceptions could occur when data is retrieved from the system. This why a try-catch statement is recommended here as well. Another clear error can be returned when something goes wrong.

As you can see at the bottom of the script, an ElementInfo class has been defined, which will be used to generate the JSON response. Defining your own types is recommended. This way you have full control over what is included and how it will be serialized. Avoid serializing a class from an external DLL, as there is no guarantee this will remain functional after updates.

namespace ElementsAPI_1
{
    using System.Linq;
    using System.Collections.Generic;
    using Models;
    using Skyline.DataMiner.Automation;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis;
    using Skyline.DataMiner.Net.Apps.UserDefinableApis.Actions;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;

    public class Script
    {
        private readonly List<string> _knownAlarmLevels
            = new List<string> { "Warning", "Minor", "Major", "Critical" };

        private readonly JsonSerializerSettings _serializerSettings 
            = new JsonSerializerSettings()
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
            };

        [AutomationEntryPoint(AutomationEntryPointType.Types.OnApiTrigger)]
        public ApiTriggerOutput OnApiTrigger(IEngine engine, ApiTriggerInput requestData)
        {
            if (string.IsNullOrWhiteSpace(requestData.RawBody))
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody = "Request body cannot be empty",
                    ResponseCode = (int)StatusCode.BadRequest,
                };
            }

            Input input;
            try
            {
                input = JsonConvert.DeserializeObject<Input>(
                    requestData.RawBody ?? string.Empty, _serializerSettings);
            }
            catch
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody = "Could not parse request body.",
                    ResponseCode = (int)StatusCode.InternalServerError,
                };
            }

            if (string.IsNullOrWhiteSpace(input.AlarmLevel) || !_knownAlarmLevels.Contains(input.AlarmLevel))
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody =
                        $"Invalid alarm level passed, possible values are: ${string.Join(",", _knownAlarmLevels)}",
                    ResponseCode = (int)StatusCode.BadRequest,
                };
            }

            List<ElementInfo> elements;
            try
            {
                elements = GetElements(engine, input);
            }
            catch
            {
                return new ApiTriggerOutput()
                {
                    ResponseBody = "Something went wrong fetching the Elements.",
                    ResponseCode = (int)StatusCode.InternalServerError,
                };
            }

            return new ApiTriggerOutput
            {
                ResponseBody = JsonConvert.SerializeObject(elements, _serializerSettings),
                ResponseCode = (int)StatusCode.Ok,
            };
        }

        private List<ElementInfo> GetElements(IEngine engine, Input input)
        {
            var elementFilter = new ElementFilter();
            switch (input.AlarmLevel)
            {
                case "Minor":
                    elementFilter.MinorOnly = true;
                    break;
                case "Warning":
                    elementFilter.WarningOnly = true;
                    break;
                case "Major":
                    elementFilter.MajorOnly = true;
                    break;
                case "Critical":
                    elementFilter.CriticalOnly = true;
                    break;
            }

            var rawElements = engine.FindElements(elementFilter);

            var elements = new List<ElementInfo>();
            foreach (var rawElement in rawElements)
            {
                elements.Add(new ElementInfo()
                {
                    DataMinerId = rawElement.RawInfo.DataMinerID,
                    ElementId = rawElement.RawInfo.ElementID,
                    Name = rawElement.ElementName,
                    ProtocolName = rawElement.ProtocolName,
                    ProtocolVersion = rawElement.ProtocolVersion,
                });
            }

            return elements.Take(input.Limit).ToList();
        }
    }
}

namespace Models
{
    public class ElementInfo
    {
        public int ElementId { get; set; }

        public int DataMinerId { get; set; }

        public string ProtocolName { get; set; }

        public string ProtocolVersion { get; set; }

        public string Name { get; set; }
    }

    public class Input
    {
        public string AlarmLevel { get; set; }

        public int Limit { get; set; }
    }
}

Publish the script

When the API script is complete, it needs to be published to the DataMiner System. You can do so using the built-in publish feature of DIS. Make sure that DIS can connect to the DataMiner System you want to upload your script to. You will need to edit the DIS settings so the DMA is selectable:

  1. In the Solution Explorer, double-click ElementsAPI.xml.

    Automation script XML

  2. At the top of the code window, click the arrow next to the Publish button and select the DataMiner System you want to upload the script to.

    Publish to DMA

Step 2: Create an API token

To access the API, you will need an API token. You can reuse the one you created in the hello world tutorial, or you can create a new one.

  1. Open DataMiner Cube, and log into your DataMiner System.

  2. Go to System Center > User-Defined APIs.

  3. Under Tokens, click Create

  4. For the Name, enter "ElementsToken", and click Generate token.

    Create API token

  5. Copy the generated secret to a safe location (e.g. a password manager).

    Create API secret

    Important

    After closing this window, you will no longer be able to retrieve the secret. Make sure that the secret is saved somewhere safe. If it is lost, a new token will have to be created.

Step 3: Create the API Definition

The next step is to create an API definition that ties the token and script together.

  1. Open the Automation module in DataMiner Cube.

  2. Open the ElementsAPI Automation script.

  3. At the bottom of the screen, click the Configure API button.

    Configure API button

  4. Optionally enter a description for the API.

  5. For the URL, enter "elements".

  6. In the bottom Tokens pane, select the ElementsToken, which was created in the previous step.

  7. Finally, click the Create button.

    Create API window

Step 4: Trigger the API using Postman

The API is now ready to be tested using an API testing app like Postman. If you have never used Postman before, check step 5 of the hello world tutorial for more detailed information.

  1. Open Postman and click the + icon to create a new request.

  2. In the URL field, enter https://HOSTNAME/api/custom/elements, replacing "HOSTNAME" with the hostname of the DataMiner Agent.

  3. Go to the Authorization tab and select Bearer Token as Type.

  4. In the Token field, enter the API token secret that you copied in step 2.

    Postman UI new request

  5. Click the blue Send button.

    You should get a Bad Request status code with the validation error that was added in the script. This happens because no body was provided in the request. This proves that the validation logic works as expected.

    Postman UI response error

  6. Open the Body tab of the request.

  7. Change the body type from none to raw in the dropdown.

  8. Change the content type from text to JSON.

    Postman UI add request body

  9. Enter the JSON input below the selectors.

    {
        "alarmLevel": "Minor",
        "limit": 10
    }
    
  10. Click the blue Send button again.

    The API will be triggered, and all elements that have minor alarms should be returned.

    Postman UI response

This is only a simple example of what can be done using the input and output functionality of a user-defined API. Numerous extensions could be made. For example, a list of alarm levels could be provided in the input, or a flag (i.e. bool property) could be added to the response that indicates whether there are more elements than the provided limit.