Creating a Custom Step

The Snippet Framework executes reusable snippets known as steps. A step should consist of a single purpose (for example, parse string to JSON, select a value from a dict, etc). Steps are chained together during execution to collect, process, and select the correct values with little to no coding required. A valid step can be a class or function, depending on where the step is registered. If a step does not exist for your scenario, you can extend the Snippet Framework to add that functionality. The two available step types are as follows:

  • Requestor - Retrieves the required data from the datasource.

  • Processor - Perform an action on the result. For example, a processor may parse, transform, or format the data. These are only a few examples of what actions a processor may perform.

Considerations

Before developing a step, ensure that there is not already a step or a way to chain steps together to accomplish the task. This helps maintain a healthy set of steps without overlap and reduces the chances of outdated code being propagated to different Dynamic Applications and/or SL1 stacks.

A core concept is the difference between a Parser and Selector. A Parser should convert a data structure into a consumable format for a Selector. By separating these two concepts you can produce a step that is more reusable than if a single step performed both actions.

Ensure to use security best practices when developing a step. For example, avoid logging any secure information (such as credentials).

Determine if the step you are developing will be used by one or many Dynamic Applications. If you know that this step will not be used by multiple Dynamic Applications, having the source code in the Snippet may prove to be more beneficial for quick adjustments and ease of deployment. If the step will be used by multiple Dynamic Applications it will be beneficial to include the code in a ScienceLogic Library to eliminate the need to copy, paste, and potentially update it in multiple locations.

Pre-requisites

There are several important concepts to understand prior to starting development of a step. These concepts help ensure the step can be written with a minimal amount of code by de-duplicating repeated code.

Type Checking

Note

Type Checking is disable by default. To enable it, go to Enable Type Checking

The framework can validate the parameters data type of a step using pytypes, a typing toolbox. For this validation to occur, you must specify the argument name and expected type(s). This type checking is available when you utilize the decorator @typechecked. If the data type is not correct an exception will be raised which will stop the ResultContainer from continuing throughout the collection pipeline.

silo.low_code.typechecked(*v, **k)

Decorator to check the input and output type of a function or method.

Perform type-checking at run-time to ensure the parameters have the correct types. If an incorrect type is provided, an exception will be raised stating the parameter and type issue.

Parameters
  • input (object) – The expected input type. If the typechecking is running against a Requestor it will use request as input. If the typechecking is running against a Processor it will use result as input. If the input is not correct, the exception InputTypeError will be raised.

  • output (object) – The expected output type. If the output is not correct, the exception OutputTypeError will be raised.

  • **extra (object) – Used when checking additional parameters. The name must match the parameter. If the input is not correct, the exception InputTypeError will be raised.

Below we can see an example utilizing typechecked to ensure the input, output, and action_arg are all of the correct type.

@register_processor(
    metadata={
        "author": "ScienceLogic",
        "descr": DESCR_PROCESSOR_SELECTOR_SIMPLE_KEY,
        "title": "Simple Key",
        "group": "Selectors",
    },
    arg_required=True,
)
@typechecked(input=Iterable[Any], output=Any, action_arg=Union[Iterable[Any], String23, int])
def simple_key(result, action_arg):
    """For dicts, lists, etc, return the keys specified with periods separating.

    For example: Path.to.value.
    """
    keys = action_arg
    if isinstance(action_arg, (str, unicode)):
        keys = action_arg.split(".")
    elif not isinstance(action_arg, list):
        keys = [action_arg]

    response = []
    if isinstance(result, list):
        if len(keys) == 1 and isinstance(keys[0], int):
            return result[action_arg]
        for value in result:
            response.append(get_parts(value, keys))
        return response

    return get_parts(result, keys)


Note

String23 is a defined type that makes Python 2 and Python 3 strings compatible.

Warning

@typechecked does not work on Python3

Enable Type Checking

To enable Type Checking, set the typechecking flag to True in the entrypoint.

snippet_framework(
    collections,
    custom_substitution,
    snippet_id,
    app=self,
    typechecking=True,
)

Warning

Note that enabling Type Checking affects the collection performance.

Below are the times of Type Checking enable and then disable.

[em7admin@CRUCIBLE-CU-188-201 framework]$ silo_mysql -e "select
avg(elapsed_time), avg(cpu_time) from silo_metrics.da_collection_jobs
where collection_time = '2022-03-23 19:30:00' and complete = 1"
+-------------------+---------------+
| avg(elapsed_time) | avg(cpu_time) |
+-------------------+---------------+
|          739.5584 |      732.1279 |
+-------------------+---------------+
[em7admin@CRUCIBLE-CU-188-201 framework]$ silo_mysql -e "select
avg(elapsed_time), avg(cpu_time) from silo_metrics.da_collection_jobs
where collection_time = '2022-03-23 21:30:00' and complete = 1"
+-------------------+---------------+
| avg(elapsed_time) | avg(cpu_time) |
+-------------------+---------------+
|           95.7932 |       91.2887 |
+-------------------+---------------+
[em7admin@CRUCIBLE-CU-188-201 framework]$

Available Parameters

Steps use inspection to determine the parameters for the functions. This allows you to determine what information is required for your step and not receive any arguments that you will not consume for the operation. Some steps may have a different set of available parameters which will be identified in the list.

All steps have the following parameters available:

  • object action_arg: Action argument for the current step

  • int action_number: Action number assigned to the execution of the action

  • object result: Result from the previous action

  • ResultContainer result_container: Steps from the previous action

  • object snippet_arg: The Snippet Argument for the collection (after substitution)

  • silo.apps.cache.CacheManager step_cache: CacheManager for interacting with step cache

Requesters require one of the parameters below to be used (in addition to any of the parameters in the previous section):

  • request: A single ResultContainer will be given to the Requestor. The Requestor will be called in a loop to handle each request. All error handling occurs in the Snippet Framework.

  • requests: List of all ResultContainers for the Requestor will be provided. The Requestor must perform error handling to ensure that all requests are processed.

A Requestor’s rewind function has access to additional parameters:

  • object data_request: Data raised by a step that contains information regarding the next request.

Suppose we are writing a step call function and need access to the result and action argument to perform a replace. To accomplish that task, we would have the following signature:

def call(self, result, action_arg):
    return result.replace(action_arg[0], action_arg[1])

call("Hello, World!", ["Hello", "Hi"])

# returns
"Hi, World!"

Logging

There are two available loggers, contextually-aware loggers and module-level loggers. Both loggers can be requested from silo.low_code and will be outlined in their respective sections.

If you need to log messages and are not within context (for example, initializing an object) you can log messages that will not be contextually aware. This logger is acquired by requesting a Module Logger.

If you require contextually-aware messages you must use the contextually-aware logger. This logger uses information from the current step and ResultContainer to generate a header to identify the message. It is also used to log to the specific policy logfile (if configured). This logger can be acquired by requesting a Step Logger to add contextually-aware information to the log message.

To see additional information about log files you can review Logfile Locations.

By default, the Snippet Framework will attempt to redact any information related to secrets. If additional secrets are required, you can add to secret by calling Add Secret. This will ensure that any Snippet Framework based logger will redact this information and replace it with *’s.

ScienceLogic Libraries and Execution Environments

The Snippet Framework makes use of reusable steps through ScienceLogic libraries. When developing a step you should strive to use these libraries for the step code. To learn more about ScienceLogic Libraries and Execution Environments you can review the docs.

ResultContainer

The ResultContainer is the standard data interface for moving information between the steps / steps. It is a pure data-class with no attached functions so its only purpose is to move information around. To learn more about the ResultContainer you can review the ResultContainer documentation.

Collection

A Collection contains all pertinent information regarding a specific Dynamic Application Collection Object. To learn more about Collection you can review the Collection docs.

Decorators

The Snippet Framework uses decorators to help users create and register steps or add functions to specific steps of the collection. The purpose of decorators is to provide a mechanism to create definitions without the need to modify the content library and to use the snippet code directly.

After calling a decorator, the defined function must follow the signature.

@decorator_name(parameters)
def name_function(result, action_arg, resultcontainer):

Step Registration Decorators

Step registration decorators are used for adding custom steps into the Snippet Framework. This enables you to easily extend the base functionality of the Snippet Framework. The available registration decorators are as follows:

silo.low_code.register_requestor(*args, **kwargs)

Decorator for registering Requesters

Register a Requester that will be validated once the Framework is initialized. A Requester must inherit from the class silo.low_code.RequestMaker.

Parameters
  • get_req_id (callable) – Optional. Function used for generating the request id

  • name (str) – Step Name to use for the Custom Step

  • metadata (dict) – Metadata related to the step

  • rewind (callable) – Callable function that updates the action_arg to start the rewinding process

  • required_args (list) – List of required top-level arguments for the step. If these arguments are not present in the action_arg, the Snippet Framework will not attempt to execute the collection and log a warning message instead.

  • arg_required (bool) – If an argument is required for a step. This denotes a step must have an argument, regardless of the type. This can be used in conjunction with required_args if your step accepts a dictionary or a single value.

Returns

Original object

Return type

object

silo.low_code.register_processor(*args, **kwargs)

Decorator to register a processor

Register a processor that will be validated once the Framework is initialized. There are no sanity checks on a processor so it should always be valid.

Parameters
  • get_req_id (callable) – Optional. Function used for generating the request id

  • name (str) – Step Name to use for the Custom Step

  • metadata (dict) – Metadata related to the step

  • type (str) – Type of the step.

  • required_args (list) – List of required top-level arguments for the step. If these arguments are not present in the action_arg, the Snippet Framework will not attempt to execute the collection and log a warning message instead.

  • arg_required (bool) – If an argument is required for a step. This denotes a step must have an argument, regardless of the type. This can be used in conjunction with required_args if your step accepts a dictionary or a single value.

Returns

Original object

Return type

object

silo.low_code.register_cacher(*args, **kwargs)

Decorator to register a cacher

Register a cacher that will be validated once the Framework is initialized.

Parameters
  • get_req_id (callable) – Optional. Function used for generating the request id

  • name (str) – Step Name to use for the Custom Step

  • metadata (dict) – Metadata related to the step

  • read (callable) – Callable function that performs a cache read for fast-forwarding

  • required_args (list) – List of required top-level arguments for the step. If these arguments are not present in the action_arg, the Snippet Framework will not attempt to execute the collection and log a warning message instead.

  • arg_required (bool) – If an argument is required for a step. This denotes a step must have an argument, regardless of the type. This can be used in conjunction with required_args if your step accepts a dictionary or a single value.

Returns

Original object

Return type

object

Development

Requestor

A Requester states how to retrieve information from a single source-type. The step will handle any device issues (credential, connection, response, etc) and will raise the appropriate exception to be handled by the Snippet Framework automatically. This step should not do any processing on the retrieved data and return an object that can be serialized.

Requestors must be a class that inherits from silo.low_code.RequestMaker.

Requestors utilize a de-duplication process to ensure the minimum number of calls are made to the device which reduces load on SL1 and the device. This process uses the function get_request_id to determine which calls are duplicates and makes the call only once to the device.

If writing a Requestor to handle all requests at once, you must implement error handling for each request. If the Requestor handles a single request at a time, error handling is provided by the Snippet Framework. In this scenario, if a silo.apps.errors.DeviceError is raised a device log message will be created on the device.

A Requestor can be configured to support the rewind capability. This allows a Processor to rewind to the previous Requestor. The rewind function should generate the new action_arg for the step. After it is executed, the request_ids will be regenerated as new request will be sent.

To utilize this feature, the step must be registered with a keyword argument rewind that is a callable function. This feature is used in conjuction with the parameter, data_request that supplies all information for the request.

Note

When developing a Requestor that supports rewind, ensure that you communicate everything required to rewind. This allows Processor developers to supply the correct format and information to the rewind function.

Example

This sample Data Requester will return the first argument as the value from the data request.

@register_requestor(
    metadata={
        "author": "ScienceLogic",
        "descr": DESCR_REQUESTOR_STATIC_VALUE,
        "title": "Static Value",
    },
)
class StaticValueRequest(RequestMaker):
    def validate_request(self, request):
        """Ensure the incoming ResultContainer is valid for the Requestor

        Validate the ResultContainer is valid. If it is invalid, raise an exception to indicate
        the ResultContainer cannot be collected by the Requestor.

        :param ~silo.low_code.ResultContainer request: Request to validate
        """
        if not isinstance(request.credential.id, int):
            msg = "Somehow you ended here in the test code! Try a different credential"
            raise CredTypeNotSupported(msg)

    @staticmethod
    def get_name():
        return "static_value"

    @staticmethod
    def get_desc():
        return "Returns whichever value is in the request"

    @typechecked(input=ResultContainer, output=ResultContainer)
    def call(self, request, action_number):
        """Returns the first element in the requests

        :param ~silo.low_code.ResultContainer request: ResultContainer that
            need to be executed
        :param int action_number: Action number assigned to the execution of the action

        :return: ResultContainer where the result is value passed in
        :rtype: ~silo.low_code.ResultContainer
        """
        return ResultContainer(request.current_config, previous_result=request)

Processor

A Processor should perform a single operation on a result. Their input should be type-checked to reduce duplicate code and reduce potential issues. A Processor can be a class or function.

A Processor can additionally request more data if the previous Requestor supports the rewind functionality. A step that utilizes this feature must register with the correct type.

@register_processor(type=silo.low_code.REQUEST_MORE_DATA_TYPE)

Before you create a new step that utilizes rewind, you must first understand how the requestor expects the data. Once you understand how the data must be sent, the step must raise the exception silo.low_code.RequestMoreData. When raising this exception you should specify any values you want set as keyword arguments. For example, if you wanted to reference data_request.beans you would use the following:

raise silo.low_code.RequestMoreData(beans="Cool Beans")

The processor can optionally set the index (storage key) when performing the loop. If this is not specified, the request_id will be used in its place. To specify the index, use the function, result_container.set_index(index_to_use).

The processor can optionally set the maximum number of iterations when performing the loop. If this is not set, the default value of 100 will be used. To set this value, update the attribute at result_container.max_iterations.

Example

This sample Data Parser will convert the input, a JSON structure, into a Python Dictionary.

@register_processor(
    metadata={
        "author": "ScienceLogic",
        "descr": DESCR_PROCESSOR_SELECTOR_SIMPLE_KEY,
        "title": "Simple Key",
        "group": "Selectors",
    },
    arg_required=True,
)
@typechecked(input=Iterable[Any], output=Any, action_arg=Union[Iterable[Any], String23, int])
def simple_key(result, action_arg):
    """For dicts, lists, etc, return the keys specified with periods separating.

    For example: Path.to.value.
    """
    keys = action_arg
    if isinstance(action_arg, (str, unicode)):
        keys = action_arg.split(".")
    elif not isinstance(action_arg, list):
        keys = [action_arg]

    response = []
    if isinstance(result, list):
        if len(keys) == 1 and isinstance(keys[0], int):
            return result[action_arg]
        for value in result:
            response.append(get_parts(value, keys))
        return response

    return get_parts(result, keys)


Cacher

A Cacher is similar to a Processor except the operation should be writing out the current result. A Cache step does not have the ability to modify the result as its not included in the automatic cache key that is generated.

A Cacher can optionally specify the ability to read which allows the Snippet Framework to fast-forward to the step after the Cacher. This can be specified the in registration decorator utilizing the keyword argument read.

Example

This sample Cacher will write the current data to the specified key. If a key is not specified, the automatic cache key will be used.

def get_key(action_arg, result_container):
    try:
        key = action_arg.get("key", result_container.request_id)
    except AttributeError:
        key = result_container.request_id
    return key


def cache_read(action_arg, result_container, step_cache):
    return step_cache.read(get_key(action_arg, result_container))


@register_cacher(read=cache_read)
def cache_write(result, action_arg, result_container, step_cache):
    step_cache.write(get_key(action_arg, result_container), result)