Scripting

Capabilities

Capability

Support

Comment

Back-pressure

Supported feature

See Backpressure and buffering

Read/update values

Supported feature

See Read update values

Logging

Supported feature

See Logging

Milestone trigger event

Supported feature

See Milestone

Importing script

Supported feature

See Import scripts

Scheduling executions

Supported feature

See Scheduled executions

Interact with alarms

Supported feature

See Alarms manipulation

Interact with collections

Supported feature

See Collections

Sending notification (SMS/EMAIL)

Supported feature

See Communications

Get the list of users

Supported feature

See Keycloak users

Format JS dates

Supported feature

See Date converter and formatter

Generate random UUID

Supported feature

See Generate UUIDv4 ids

Sending SNMP TRAP

Supported feature

See Sending SNMP TRAP

Replaying SNMP TRAP

Supported feature

See SNMP traps debug replay

HTTP request

Supported feature

See HTTP requests

Concepts

Overview

The osp-scripts plugin provides the ability to execute scripts written in Javascript and to interact with hierarchy elements and external systems.

Behavior

The osp-scripts provides a Javascript engine based on GraalVM, on which scripts are run on transient contexts, meaning that they do not have persistent state on their own (setting a value to a variable defined in the global scope of the script will be forgotten in the next script execution).

Scripts come in two forms :

  • Value scripts : Those scripts are bound to a value in the hierarchy and their result is used to generate the value content. They provide a way of computing a complex state and provide it to the hierarchy.

  • Detached scripts : Those scripts are detached from the hierarchy. They provide a way to execute actions on the hierarchy or on external systems.

Scripts executions follow the following constraints :

  • A script is guaranteed to have the view of the hierarchy as it was when it was triggered, independently of potential execution delays

  • If a value triggers the execution of multiple scripts, all scripts are guaranteed to have the same hierarchy view

  • Executions of different scripts are always run in parallel

  • Executions of the same value script are always run sequentially, for coherent update of their value

  • Executions of the same detached script are always run in parallel

Script execution

The script execution use a shared pool of threads, this pool must fit the need of the current installation with a tradeoff between :

  • Memory usage

  • Context switching

  • I/O duration

Warning

In general, the number of threads should be increased in case of a high number of scripts running at the same time. The more scripts are idle (like waiting on an external system), the more threads can be increased to increase throughput. But there is a drawback, each thread consumes memory and in case of too many context switches, the overall performance of the system can be affected.

Concurrency

A value script (describe in owner.scripts) is always executed in a concurrent way to avoid creating some edge cases on the impacted value.

A detached script can be concurrent so it’s must be taken into account that all interactions of the scripts on external system can arrive in any order :

For example a simple script logging in into a system and then performing a task can fail in concurrent way, if multiple login is not supported :

The intended script execution

scriptExecution1 -> WebSite : Login
WebSite -> scriptExecution1 : 200, ok
scriptExecution1 -> WebSite : doSomeAction
WebSite -> scriptExecution1 : 200, ok

The executed script in concurrency

scriptExecution1 -> WebSite : Login
WebSite -> scriptExecution1 : 200, ok
scriptExecution2 -> WebSite : Login
WebSite -> scriptExecution2 : 200, ok
WebSite -> scriptExecution1 : Connection closed (new connection)
scriptExecution1 -> WebSite : doAction
WebSite -> scriptExecution1 : 401, unauthenticated
scriptExecution1 -> WebSite : doSomeAction

Backpressure and buffering

When a script (value or detached) is called, it’s executed at most X times depending of the concurrency value set.

If the script execution is slower than the execution schedule frequency, the system will bufferize the newly added scripts executions.

When the buffer is full (each value has his own buffer), the system will use one of the backpressure strategy :

  • drop_older (drop the old values)

  • drop_latest (drop the new values)

Trigger information

Scripts can access the reason that triggered their execution by global variable trigger (trigger.id, trigger.content, …).

A trigger has the following structure :

  • id : the id of the value (string)

  • name : the name of the value (string)

  • description : the description of the value (string)

  • content (nullable) : the content of the value, can be null in case of reading errors (boolean, number or string depending on the type field)

  • error : the error in case of error, empty otherwise (string)

  • type : BOOLEAN, INTEGER, DECIMAL, TEXT (string)

  • timestamp : the timestamp attached to the value (integer)

Note

For detached scripts executed manually (i.e., not from callbacks, like an explicit call to scripts.run()), trigger only provides following fields :

  • id : always contains internal.manual, useful to determine if script was triggered manually or by a callback (string)

  • parameters : parameters list that were provided to the script (string[])

Warning

Owner script can only be triggered by callbacks. Manual actions like Actions can’t trigger an owner script.

Read update values

Scripts can access values from the hierarchy :

  • Read a value with values.get(value_id: string)

  • Update a value (only works for variable values) with values.update(value_id: string, content: any)

  • Value scripts can access their current value using the global variable myself (myself.id, myself.content, …)

A value has the following structure :

  • id : the id of the value (string)

  • content (nullable) : the content of the value, can be null in case of reading errors (boolean, number or string depending on the type field)

  • error : the error in case of error, empty otherwise (string)

  • type : BOOLEAN, INTEGER, DECIMAL, TEXT (string)

  • timestamp : the timestamp attached to the value (integer)

Note

To be able to read a value, the script must first declare that value in its accessed values.

Logging

Logs information with log with the following level:

  • trace

  • debug

  • info

  • warn

  • error

It is possible to include an element with the syntax log.warn("The error was {}", error)

log.info("Starting script.");
let message = { test: 1 };
log.warn("Message with parameter part {}", message);

Import scripts

Inside your scripts you may need some additional modules or functions. This section explain how to import them in your configuration. Here are some restrictions:

  • You need the source code you want to include (no package importation).

  • Only scripts with .mjs file extension can make use of the import keyword (it will be ignored if the extension is simply .js).

  • Objects or functions you want to import have to be marked with the export keyword.

Note

If you have a script with .js extension, you can freely rename it to .mjs.

A script can be imported with the following step:

  1. Add you script in your configuration directory (we will call it <script_path>).

  2. Add an entry in the file : modules/scripts/<module_script>/module.resources:

"resources": [
    {
        "source": "<script_path>",
        "destination": "<script_path>"
    }
]
  1. Add an import statement in your script:

import { my_function } from "<script_path>";

Note

The path to your script can be relative or absolute.

You may want to share a lib through all your configuration and for every script. The previous method is not very friendly and may result to an irrelevant file structure. For every library that may be shared between multiple scripts, the best practice is to place all of them outside the root folder :

  1. Create a lib directory at the root of your project and put every scripts that you want to be global inside it :

<pre>
...
├── certs
├── keys
├── lib
│   └── my_lib.mjs <-- this file
├── modules
├── offline-license-request
├── root
├── schema
├── stack
...
</pre>
  1. Then place every scripts that you need to be global inside the lib directory. Now we need to edit the modules/scripts/<module_script>/module.resources file and link the folder lib at in the root folder. Here is an example :

{
    "resources": [
        {
        "source": "lib/",
        "destination": "root/"
        }
    ]
}
  1. Finally, we can access to our script using the import statement like this:

import { my_function } from "my_lib.mjs";

Warning

When migrating a script from/to .mjs, be careful to check your script. The JavaScript flavor used by the *.mjs is ECMAScript 6.

Scheduled executions

Scripts executions can be scheduled periodically using cron expressions. The precision of those expressions is at the second, for example * * * * * ? * will schedule a script every second.

Schedule execution is setup in either owner.scripts or detached.scripts with scheduledExecutions key.

There is some good resources online for helping you generate cron entries Quartz generator help

Milestone

OnSphere scripts have features from module Milestone exposed.

All those features are available through the milestone class

Hint

Milestone features require a milestone module.

Class description

Name

Attribute

Description

Return

triggerEvent(serverId, eventId)

serverId is the ID of the milestone server that owns the event to trigger. eventId is the Milestone event id (often called event “type”).

Send an event trigger request to milestone server.

Return triggered event.

Example

The following example sends a trigger request to the c376083c-8e1a-4896-9e86-a94ec4ed66ab event configured on the root.server milestone server.

function main() {
  const event = milestone.triggerEvent("root.server", "c376083c-8e1a-4896-9e86-a94ec4ed66ab")
  log.info("{}", JSON.stringify(event))
}

main()

When the script is executed, the module sends a request to Milestone API triggering the specified event. The event variable will contain the triggered event.

Communications

A script can trigger communications with communications

  • Sends a notification with a static contact list with sendStaticNotification(notificationId, originator, providers, contactId, trigger)

    • Parameters

      • notificationId : The id of the notification to send.

      • originator : The notification source.

      • providers : The list of providers to use.

      • contactId : The id of the contact to send the notification to

      • trigger : An object containing arbitrary information that will be provided to the message generation.

    • Example:

      communications.sendStaticNotification(
        "root.communication.notification",
        {
              smsSender: "Script",
              emailSenderAddress: "script@localhost",
              emailSenderName: "my super script"
          },
        [
          {
              providerId: "root.communication.provider.maildev"
          }
        ],
        "root.communication.directory",
        {
              name: "Script",
              content: true,
              timestamp: 0
          }
      );
      
  • Sends a notification with a contacts list extracted from a collection with sendCollectionNotification(notificationId, originator, providers, collectionId, collectionFilter, trigger)

    • Parameters

      • notificationId : The id of the notifications to send.

      • originator : The notification source.

      • providers : The list of providers to use.

      • collectionId : The id of the collections from which to extract the contacts to whom the notification must be sent.

      • collectionFilter : The filter to apply on the collection when extracting contacts.

      • trigger : An object containing arbitrary information that will be provided to the message generation.

  • Sends a notification with a contacts list extracted from Keycloak with sendKeycloakGroupNotification(notificationId, originator, providers, wantedGroups, trigger)

    • Parameters

      • notificationId : The id of the notification to send.

      • originator : The notification source.

      • providers : The list of providers to use.

      • wantedGroups : The list of groups to send the notification to. If empty, everyone will be notified.

      • trigger : An object containing arbitrary information that will be provided to the message generation.

Collections

Collections can be manipulated with collections.

Each request except getUsername and getUserAttributeFromPreferences return a result object as following:

  • success: if request is a success or not (true/false)

  • message: contains a message with details on why the request failed. Empty if request is successful

  • content: data returned from a successful request. Type of the data is different for each request type. Each operation presented below explain what you should expect from the content of the request

Gets one or multiple elements from a collection:

  • list: Returns the list of elements from one collection that match the given filter id from the schema configuration (can be omitted). Specifies the number of elements (pageSize) and the offset (pageNumber, starts at 0). Returned object contains one totalCount property with the number of elements found (unbound by the pageSize) and collections as an array of documents.

    • collections.list(schema: String, pageSize: int, pageNumber: int)

    • collections.list(schema: String, pageSize: int, pageNumber: int, filterId: String)

  • listCustomFilter: Returns the list of element from one collection matching the given custom filter provided. Same return type as list.

    • collections.listCustomFilter(schema: String, pageSize: int, pageNumber: int, filterId: String)

  • get: Returns the first element from one collection matching the given filter. Returns directly the document.

    • collections.get(schema: String, documentId: String): Filter by document id.

    • collections.getWithFilterId(schema: String, filter: String): Filter by filter from schema configuration.

    • collections.getWithCustomFilter(schema: String, filter: String): Filter by custom filter.

Inserts/Updates an element from a collection. Each one of these request returns an object with documentId and operation (insert or update):

  • insert: Inserts a new element into a collection.

    • collections.insert(schema: String, data: Object)

  • update: Updates a given element from a collection.

    • collections.update(schema: String, documentId: String, data: List<UpdateMethod>): Updates by giving a list of updates to apply on the element. You can look at the collection documentation to see how to define an update object.

    • collections.update(schema: String, data: Object): Updates by giving the complete updated object.

  • delete: Deletes a given element from a collection.

    • collections.delete(schema: String, documentId: String)

Get username of authentified user:

  • getUsername: returns directly the username (string) of the authentified user. This information is accessible only if the script is trigger by a front-end action, like a menu for example. Otherwise, this will return an empty string.

Get an attribute from an user preferences:

  • getUserAttributeFromPreferences(attribute: String, defaultValue: any): get the value associated with attribute from the currently authentified user preferences. If no value is found, defaultValue is returned instead. defaultValue is not a mandatory parameter. If not specified, null is returned instead.

Lookup

Gets information from a lookup file with lookup:

  • has: Returns a boolean indicating if the element exists (string filename, string key, int column).

  • hasLine: : Returns a boolean indicating if the line corresponding to the key exists (string filename, string key).

  • get: Returns a string corresponding to the key and column (string filename, string key, int column).

  • getLine: Returns a list of string corresponding to the key (string filename, string key).

  • get: Returns a string corresponding to the key and column if the column is missing the default value is returned (string filename, string key, int column, string defaultValue).

  • getLine: Returns a list of string corresponding to the key if the line is missing the default value is returned (string filename, string key, string[] defaultValues).

Example to check if a value exists and get it.

if (lookup.has("file.lookup", "35665", 0)) {
    let infos = lookup.get("file", "35665", 0);
    console.log(infos);
    return true;
}
return false;

Sending SNMP TRAP

Sends snmp-trap with snmp:

  • getTarget: Returns a new target to use to send a trap (string host, int port, string community).

  • createTrapV1: Returns a trap for the snmp version 1 (string Entreprise oid, string source ip address).

  • createTrapV2: Returns a trap for the snmp version 2 (string Entreprise oid, string source ip address).

  • createTrapV3: Returns a trap for the snmp version 3 (string Entreprise oid, string source ip address).

  • sendTrap: Sends a trap (SnmpTrap trap, SnmpTarget target).

The target offers the following function:

  • setRetry: Sets the number of retry (int retry).

  • setTimeoutS: Sets a new timeout in second (int second). Default: 1.5 seconds.

  • setTimeoutMs: Sets a new timeout in millisecond (int second). Default: 1500 milliseconds.

The trap offers the following function:

  • addOctetStringVariable: Adds an OCTET_STRING variable to the trap (string oid, string content).

  • addIntegerVariable: Adds an INTEGER variable to the trap (string oid, int content).

  • addCounter32Variable: Adds a COUNTER_32 variable to the trap (string oid, int content).

  • addCounter64Variable: Adds a COUNTER_64 variable to the trap (string oid, long content).

  • addGauges32Variable: Adds a GAUGES_32 variable to the trap (string oid, int content).

  • addTimeTicksVariable: Adds a TIME_TICKS variable to the trap (string oid, int content).

  • addUnsignedIntegerVariable: Add an UNSIGNED_INTEGER variable to the trap (string oid, long content).

Example to send a trap.

let target = snmp.getTarget("10.110.0.116", 162, "public");
let trap = snmp.createTrapV2("1.3.6.1", "8.8.8.8");
trap.addOctetStringVariable("1.3.6.1.2.1.1.5", "My first trap");
trap.addOctetStringVariable("1.3.6.1.2.1.1.1", "The value is 10");

snmp.sendTrap(trap, target);

SNMP traps debug replay

It is possible to replay a trap that was produced by the osp-snmp-trap module if its raw data are available. This is typically done by enabling the trap raw data storage in the alarm on osp-snmp-trap (alarm.storeRawData = true) and then accessing it using a menu interaction (alarm.additionalData._rawData) that will trigger a detached script.

  • snmp.replayTrap(data: byte[]) : asks a running osp-snmp-trap module to replay the trap from its given raw data.

Here is an example of a replay trap script :

main();

function main() {
  log.info("Replay trap requested");
  snmp.replayTrap(trigger.parameters[0].raw);
}

and its triggering menu configuration interaction :

{
"label": "Replay trap",
"icon": "replay",
"context": [
    {
    "type": [
      "AlarmTable",
      "AlarmHistoryTable"
    ],
    "action": "root.actions.run_script",
    "condition": "${alarms.selected}.length === 0 && !!${alarms.clicked.additionalData} && !!${alarms.clicked.additionalData._rawData}",
    "input": {
        "scriptId": {
        "expression": "'root.replay-trap'"
        },
        "arguments": {
        "expression": "[{'raw': ${alarms.clicked.additionalData._rawData}}]"
        }
    }
    }
]
}

HTTP requests

HTTP/HTTPS requests can be issued with http. The CA certificate must be defined on certs/external.

  • Gets a request object to fill

    • http.getRequest(): HttpScriptRequest

  • Fills a request

    • request.addHeaders(key: string, value: string): HttpScriptRequest

    • request.setBody(body: string): HttpScriptRequest

    • request.setBody(body: byte[]): HttpScriptRequest

    • request.setBody(body: string, mimeType: string): HttpScriptRequest

    • request.setBody(body: byte[], mimeType: string): HttpScriptRequest

    • setCredentials(username: string, password: string): HttpScriptRequest

    • setTimeout(timeoutInMilliSeconds: number): HttpScriptRequest

    • request.notFollowRedirects(follow: boolean): HttpScriptRequest

  • Sends requests

    • http.doGet(url: string): HttpScriptResponse

    • http.doGet(url: string, request: HttpScriptRequest): HttpScriptResponse

    • http.doPost(url: string): HttpScriptResponse

    • http.doPost(url: string, request: HttpScriptRequest): HttpScriptResponse

    • http.doPut(url: string): HttpScriptResponse

    • http.doPut(url: string, request: HttpScriptRequest): HttpScriptResponse

    • http.doDelete(url: string): HttpScriptResponse

    • http.doDelete(url: string, request: HttpScriptRequest): HttpScriptResponse

  • Accesses request response

    • response.isSuccess(): boolean

    • response.getStatusCode(): number

    • response.getHeader(): string

    • response.getBody(): string

    • response.getError(): string

Example to connect with a username and password.

let request = http.getRequest();
request.setCredential("test", "test");
let response = http.doGet(
"http://httpbin.org/digest-auth/auth/test/test",
request
);

if (response.getStatusCode() != 200) {
return false;
}

return true;

Alarms manipulation

Alarms can be created and manipulated with alarms.

  • Creates an alarm (Directly added to the live collection without being processed by the pre-insertion rules)

    • alarms.create(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[])

    • alarms.create(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], additionalData: object)

    • alarms.create(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], hideUntil: long)

    • alarms.create(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], additionalData: object, hideUntil: long)

  • Inserts an alarm (Added to the live collection after being processed by the pre-insertion rules)

    • alarms.insert(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[])

    • alarms.insert(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], additionalData: object)

    • alarms.insert(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], hideUntil: long)

    • alarms.insert(severity: int | string, summary: string, location: string, source: string, serial: string, tags: string[], additionalData: object, hideUntil: long)

  • Acknowledges alarms

    • alarms.acknowledge(id: string | string[])

    • alarms.unacknowledge(id: string | string[])

  • Escalates alarms

    • alarms.escalate(id: string | string[], severity: int | string)

  • Locks alarms severity

    • alarms.lockSeverity(id: string | string[], severity: int | string)

    • alarms.unlockSeverity(id: string | string[])

  • Removes alarms

    • alarms.clear(id: string | string[])

  • Tags alarms

    • alarms.tag(id: string | string[], tag: string)

    • alarms.untag(id: string | string[], tag: string)

  • Adds journal entry to alarm

    • alarms.addJournal(id: string, text: string, user: string)

  • Edits alarms

    • alarms.edit(id: string | string[], summary: string, location: string, source: string, hideUntil: long, forceHide: boolean, `additionalData: object)

    • alarms.editSummary(id: string | string[], summary: string)

    • alarms.editLocation(id: string | string[], location: string)

    • alarms.editSource(id: string | string[], source: string)

    • alarms.editHideUntil(id: string | string[], hideUntil: long)

    • alarms.editForceHide(id: string | string[], forceHide: boolean)

    • alarms.editAdditionalData(id: string | string[], additionalData: object)

  • Runs an alarms actions (action.alarms). The data are the parameters passed to the action when run.

    • alarms.runActionRule(id: string, data: object)

    • alarms.runActionRule(id: string, data: object, delay: long)

The value of hideUntil must be a timestamp in milliseconds.

All requests return a result object with

  • success: boolean : indicates if the request succeeded or failed

  • message: string : a message containing additional information about the success or the failure of the request

Keycloak users

Keycloak users can be fetch in a script with:

  • users.list()

This returns directly the list of users. Each user object contains:

  • username

  • firstname

  • lastname

  • email

  • phoneNumber

  • dashboard

  • viewport

  • groups

Date converter and formatter

JS dates can be formatted by specifying the format and a timezone.

  • timestamp.format(epochMilli: long): convert a date passed as milliseconds into a string representation using dd/MM/yyyy HH:mm as default format and UTC as default timezone.

  • timestamp.format(format: string, epochMilli: long): convert a date passed as milliseconds into a string representation using a given format and UTC as default timezone.

  • timestamp.format(format: string, epochMilli: long, timeZone: string): convert a date passed as milliseconds into a string representation using a given format and a given timezone.

Generate UUIDv4 ids

You can get a randomly generated UUID in a script with:

  • idGenerator.uuidv4()

This returns directly the generated id as a string.

Scripts execution and result reporting

Detached scripts can be run from other scripts with scripts.

  • scripts.run(detached_script_id: string)

  • scripts.run(detached_script_id: string, parameters: any[])

Those requests return a result object with

  • success: boolean : indicates if the request succeeded or failed

  • message: string : a message containing additional information about the success or the failure of the request

  • result: object : holds a result generated by the detached script

Detached scripts can also interrupt their execution by reporting a result or an error with execution.

  • execution.reportResult(message: string)

  • execution.reportResult(message: string, result: object)

  • execution.reportFailure(message: string)

If a detached script does not report any result or failure, it will generate a result depending on the execution of the script (i.e. a syntax error will produce a result with success set to false and message containing a description of the error).

Remote script execution

This method execute a command via a SSH connection on a external system.

Warning

Be aware that this kind of process is heavy. In consequence avoid usage of high frequency request.

Usage :

  1. Creates the runner to the given host via the builder

  2. Executes the runner (each request create a new connection to avoid session problems)

  3. Checks the execution status

Build the connection

let runner = ssh
  .certCmdBuilder(host, port, username, certificate - file)
  .build();

Authenticate with a certificate

ssh.certCmdBuilder(host, port, username, certificate - file);
  • host : the server on witch to execute the command

  • port : the port to use for SSH connection

  • username : The username of the SSH connection

  • certificate-file : The filename of the certificate

See External resources security for details about the certificate management.

Authenticate with username password

ssh.plainTextCmdBuilder(host, port, username, password);
  • host : the server on witch to execute the command

  • port : the port to use for SSH connection

  • username : The username of the SSH connection

  • password : The certificate filename

Additional parameters configuration

Parameters

Type

Default value

Description

setConnectionTimeout

number

10

The time allowed to establish the SSH connection

setMaxCommandTime

number

10

The time allowed for the command execution, after this time the SSH connection is dropped

enableStrictHostKeyChecking

boolean

false

If set to true a change of SSH key on server will raise an error. Beware if you are running with this properties set, a change of SSH key will require a restart of the module.

disableStdIn

boolean

true

Disable the stdIn input collection, this can be used in case of large commands output (to save memory)

disableStdErr

boolean

true

Disable the stdErr input collection, this can be used in case of large commands output (to save memory)

Return value

Parameters

Type

Comment

returnCode

number

The command return code on the remote host

stdIn

string

The command STDIN output

stdErr

string

The command STDERR output

exceptionCode

number

See below

Exception code values

  • NO_EXCEPTION(0)

  • FAIL_TO_CONNECT(-1)

  • FAIL_TO_EXECUTE_CMD(-2)

  • MAX_COMMAND_EXECUTION_TIME_REACHED(-3)

Deploy certificate

Warning

OnSphere doesn’t support openssh key, use ssh-keygen -t rsa -m PEM to generate a compatible key

The certificate must be registered as an osp-script module resource and must be at the root of the folder /certs.

Example

{
    "resources": [
        {
            "source": "root/ssh/",
            "destination": "/certs"
        }
    ]
}