Driver Extension Development
Drivers empower Engines to communication over different transport channels with the outside world. One driver may allow GUARDARA to communicate over the network, another one, to communicate over the air and so on.
Unlike Fields and Transforms, Drivers only have a backend component. The frontend is dynamically generated by the user interface based on the parameters expected by the Driver’s backend component.
Driver Manifest
The manifest.json
files contain basic information about the driver extension. An example manifest file can be seen below.
{
"component": "driver",
"description": "GUARDARA TCP Network Driver",
"display_name": "Network / TCP",
"version": "1.0.0",
"license": "GUARDARA",
"url": "https://guardara.com",
"author": {
"name": "Zsolt Imre",
"email": "zsolt.imre@guardara.com"
}
}
The display_name
optional attribute can be used to define the text to represent the Driver when the driver list is displayed on the Project Configuration page.
There may be two, identical manifest files in your project. When making changes to the manifest, make sure you update both files so they are identical.
Driver Properties
The properties.json
file describe the configurable properties of the Driver. A Driver for example needs to know where to connect to in client mode, or what port to listen on in server mode. Drivers that operate by storing mutations on the file system would need to know where the files should be stored. Using the properties file developers can define what parameters the Driver expects and, also how those should be rendered on the user interface.
The properties file of a Driver however is a bit different than of other extensions’. This is mainly due to the fact that Drivers have no skeleton
file, thus that information is derived from the properties file.
The properties file is a dictionary, where each key represents the name of a property. The value of each property is an object that describe the characteristics of the property.
The most basic properties file that define only the mandatory name
property is shown below.
{
"name": {
"type": "str",
"mandatory": true,
“display”: {
"display_name": "Driver Name",
"description": "The name of the driver",
"visible": false
}
}
}
The name
property is a special one and Drivers must define it in the properties file. The value is not rendered on the user interface. However, the Engine must know which Driver to use for a given test run, thus the name of the Driver is defined using this property. The user interface populates the value of this property automatically at the time a Driver is selected during Project configuration.
Another example: If a Driver would like to allow users to enable debug mode, the properties file could be extended as shown below.
{
"name": {
"type": "str",
"mandatory": true,
“display”: {
"display_name": "Driver Name",
"description": "The name of the driver",
"visible": false
}
},
"debug": {
"type": "bool",
"default": false,
“display”: {
"display_name": "Debug Mode",
"description": "Enable to see debug messages on console"
}
}
}
The supported configuration options of the property definitions are summarized by the table below.
Property | Description |
---|---|
type | The type of the property. The value of the type property can be either a string representing a Python type or a list of strings representing multiple Python types. The supported Python types are: str , int , bool . |
mandatory | Defines whether it is required to define the property. |
default | The default value for the property. Non-mandatory properties must have a default value set. |
values | A whitelist of acceptable values. |
display.condition | To be used to define condition(s) to be evaluated in order to determine whether the property should be rendered or not. Please see the relevant section of this page for mode details. |
display.display_name | The short, human-friendly name of the property as shown on the user interface. |
display.description | The description of the property as shown on the user interface. |
display.multiline | Applicable to text fields only. Setting it to true will result in the render of a multiline text area. |
display.visible | The value of this property only has an impact on how the user interface renders the property. In case its value is false , no form control will be rendered for the property. |
display.hide | The hide property, if set true , will prevent the property to be displayed on the test details page. |
display.file | In case of string fields (where the type property is set to str ) setting this attribute to true will result in the rendering of a button, that once clicked, allows a user to select a file. The value of the selected file will be set as value for the property. |
Display Conditions
One or more conditions can be defined for properties. If a condition evaluates to true
, the property will be rendered.
Conditions can be defined either as a string, or a list. A single condition can be defined as a string as shown below.
...,
"example": {
"type": "int",
"default": 0,
"display": {
"display_name": "Example Integer",
"description": "Just an example to demonstrate single condition.",
"condition": "protocol == 'TCP'"
}
},
...
As can be seen, conditions can refer to the actual value of properties and compare it to a specific value, for example: protocol == 'tls’
and value <= 1
. In the example above, when the Project Configuration page renders the driver properties, it will query the current value of the protocol
field and, if its value is TCP
, only then the example property will be rendered.
Multiple conditions can be set by providing a list as the value of the condition
key. This is shown below.
...,
"example": {
"type": "int",
"default": 0,
"display": {
"display_name": "Example Integer",
"description": "Just an example to demonstrate single condition.",
"condition": [
"protocol == 'TCP'",
"mode == 'client'"
]
}
},
...
In the above example, the example
property is only rendered if at least one of the conditions defined evaluate to true
.
Action Properties
An Action in GUARDARA terminology is basically an activity to be performed by the Driver. The action properties are configurable via the user interface (Flow Designer) and define certain constraints such as the action timeout value and what to do in case of specific events such as timeout and connection error. This information is passed to the Driver methods in the form of an action
parameter.
All actions share the same properties; however, their value may differ. An example action properties for the Send action can be seen below.
{
"action": "send",
"timeout": 3,
"on": {
"monitor": "",
"timeout": None
},
"retry": {
"retry": True,
"count": 0,
"interval": 1
},
"meta": {
"id": "055bd123-83fc-4287-8628-582fe2c00ea4",
"open": True
}
}
The table below summarizes the properties with their descriptions.
Property | Description |
---|---|
action | The activity (e.g.: send , receive ) the action belongs to. This allows the Engines to pass the action object to the right Driver method. In the above example, the action object will be passed to the Driver’s send method. |
timeout | The timeout in seconds for the given action. As per the above example, the Engine will wait 3 seconds to establish the connection. If it cannot establish a connection in 3 seconds, appropriate action will be taken. Please note, the type of the value is float . |
on.timeout | Defines how the engine should handle an action timeout. Valid values are discussed under the Event Values section. |
on.monitor | Defines how the Engine should handle when a Monitor (if configured) reports an issue. Valid values are discussed under the Event Values section. |
retry.retry | (Optional) In case of an error or timeout, one may want to retry the operation again just in case the event was generated due to a temporary glitch (e.g., packet loss). This property defines whether a failed action should be performed again or not. |
retry.count | This property defines how many times the Driver should try to perform the action again should the action fail. |
retry.interval | This property defines how much time the Driver should wait between each retry. |
meta | Meta data set by the GUARDARA Manager. It can be ignored by the Driver. |
All Drivers should be prepared to handle the above action properties (such as retry
) as required by the implementation. Worth noting that these action properties are not only used by the Driver but also by the Engine itself in order to control the flow.
Please note, the interpretation of retry.retry
and retry.count
is up to the Driver. A Driver, similar to the network drivers, may decide to ignore the retry.retry
property and decide whether to repeat an action or not solely based on retry.count
. The network drivers, for example, attempts to retry an action if retry.count
is greater than 0
.
Event Values
The on.timeout
and on.monitor
action properties support the following values.
report
: The Engine will include the event as a finding in the report and will continue testing.skip
: The Engine will skip all remaining actions and continue from the next Flow iteration.skip_and_report
: Same as above, but the event will be included as a finding in the report.next
: The Engine will stop testing the current field and start mutating the next one starting from the beginning of the next Flow iteration.next_and_report
: Same as above, but the event will be included as a finding in the report.pause
: The Engine pauses the test execution immediately.pause_and_report
: Same as above, but the event will be included as a finding in the report.terminate
: The Engine terminates the test immediately.terminate_and_report
: Same as above, but the event will be included as a finding in the report.
Please note, that the on.monitor
property is completely irrelevant to a Driver. It is only used by the Engine.
Even though the value of the on.timeout
property is not directed to the Driver either, it may be useful for the Driver to know what will happen if it signals an action timeout to the engine. This allows the Driver to take appropriate action if needed. For example, if the value is terminate
or terminate_and_report
and the Driver signals a timeout, the Engine will most definitely terminate the test. The Driver knowing this could (optional!) terminate the connection to the target before throwing the TimeoutException
.
Please note, the Driver should report timeout and connection error events via the standard exceptions discussed under the Signalling and Exceptions section of this document.
Driver Methods
Other than the send
method below, all methods are documented in the Driver source files generated by the SDK.
The Send Method
The send method is responsible for transmitting data to the target.
The Engine passes the data to be sent as either a list of bytes
or a single bytes
. The reason for this is to support Drivers that, for example, would like to test a particular function of a shared object library and pass mutated values via one or more arguments. To be more specific:
It is possible to implement a Driver that could test individual methods of a software library. If a method accepted three arguments, we would have to create a Message Template with three Group Fields at the root, each representing the value of an argument. When generating test case based on this Message Template, the Engine renders the value of each block and passes those to the Driver’s send method as a list
. The Driver then can distinguish which value belongs to which argument based on their index within the list
.
For tests where the above is not relevant, Drivers can simply concatenate (join) the list, for example:
data = b"".join(data)
Signalling and Exceptions
Drivers are responsible for signaling some key events to the GUARDARA Engine using exceptions. In response to an exception, based on the action properties, the Engine will take appropriate action. Drivers can use the following exceptions to signal the Engine.
Exception | Description |
---|---|
TimeoutException | This exception should be thrown in case an operation has timed out. Please note, if configured, the Driver should perform retries as defined by the action object before throwing the exception. The exception class should be imported as: from guardara.sdk.exceptions import TimeoutException |
ConnectionErrorException | This exception should be thrown in case an operation has failed due to connection error. Such errors can be if it was not at all possible to establish a connection to the target or the send or receive operation failed immediately. The exception class should be imported as: from guardara.sdk.exceptions import ConnectionErrorException |
NextIterationException | Drivers should raise this exception to ask the engine to move to the next iteration. This can be useful, for example, in case of a connection error (e.g., connection reset) when the connection was dropped due to a mutation, but the event is not of interest. The exception class should be imported as: from guardara.sdk.exceptions import NextIterationException |
Exception | A generic exception should be thrown in case of unexpected errors. |
In case the Engine receives:
Exception
: The test is terminated, and the error is reported in both the activity log of the test and the Engine console.ConnectionErrorException
: Handled according to the "Connection Error" action property configured for the action within the Test Flow Template.TimeoutException
: Handled according to the "Action on Timeout" action property configured for the action within the Test Flow Template.
In the case a Monitor is configured for the test, even if the Driver raises a ConnectionErrorException
or TimeoutException
exception, The Engine queries the Monitor(s) first to see if any monitors picked up an issue. If none of the configured monitors reported an issue with the target, only then the Engine will process the "Connection Error" and "Action on Timeout" event handling as configured by the action properties.
External Resources
The SDK generates Driver templates so that any resource included under the directory of the Driver module (g_driver_<name>
) will be included in the Driver package.
Drivers can get the path to their resources via self.context.get('path')
. For example, referring to a shared object library named test.so
included with the Driver extension would look like:
mylib = self.context.get('path') + '/test.so'
Example
The example below demonstrates how a shared object library can be bundled and used within a Driver extension.
Shared Object Library
Create the header file test.h
:
#include <stdio.h>
extern void echo(char *text);
Implement the actual method in test.c:
#include "test.h"
void echo(char text[]) {
printf("GOT: %s\n", text);
}
Compile the code into a shared object library:
$ gcc -c -Wall -Werror -fPIC test.c
$ gcc -shared -o test.so ./test.o
Create a Driver
Issue the following command to create a Driver.
guardara extension -e driver -o create
Assume the name of the Driver is test
and the SDK stored the Driver under the test
directory, we use the command below to include the shared object library in the Driver:
cp test.so ./test/backend/g-driver-test/g_driver_test/
Finally, the actual Driver implementation. The below example will simply pass all generated mutations to the echo
method of the shared library.
import os
from ctypes import *
from guardara.sdk.driver.DriverInterface import DriverInterface
class Driver(DriverInterface):
def __init__(self, context, properties, session=None):
DriverInterface.__init__(self, context, properties, session)
# Load the shared library bundled with the driver.
mod_path = os.sep.join([self.context.get('path'), "test.so"])
self.shared_lib = cdll.LoadLibrary(mod_path)
def connect(self, action):
pass # Not relevant
def disconnect(self, action):
pass # Not relevant
def send(self, action, data):
# Pass the data generated by the engine to the `echo` method of the
# shared library.
self.shared_lib.echo(b"".join(data))
def receive(self, action):
pass # Not relevant
def cleanup(self):
pass # Not relevant