• First Steps
  • Basic Data Operations
  • Creating Action Handlers using Action API
  • Developing a Connector
  • Incident handling via the Domain Object Api
  • Working with Automation Tasks

Overview

In HIRO™ 7 now you can build your own action handlers that can be deployed on the HIRO™ SaaS environment or on your premises through the Action API.

For details on how the Action API works please refer to: Action API Concepts

This tutorial will demonstrate how to create a simple SSH Action Handler using the Action API

What we will do in this tutorial
  1. Create a websocket connection with the Action API

  2. Listen to the websocket connection for the action to be executed

  3. Parse the parameters so that they can be used by the user to execute an action

  4. Send the result back to Action API

Prerequisites

  • API access to a HIRO instance

  • Authenticated HIRO session with valid access token

Please follow the tutorial First Steps with HIRO to authenticate before proceeding to use HIRO’s Graph API. We assume for this tutorial that you have created an authenticated hiro session, which is available as hiro_session object:

# Initiate session
hiro_session = initiate_hiro_session('./hiro_credentials.json')

Note - Please use the Action Handler Application credentials and not the instance / engine credentials to obtain the authentication token

Install python libraries for the websocket-client

You will need the websocket-client library to create a websocket connection. In case you do not have it in your environment yet, install it with the pip command:

pip install websocket-client

Connecting to the Action API using a websocket

To create a websocket connection with the Action API we will create the following functions:

  • on_message to receive the message sent by the server to our websocket client

  • on_error to receive the error sent by the server in case of some issues encountered

  • on_close to close the websocket connection

  • on_open to open the websocket connection

  • func_websocket to ensure that the websocket is a long lived connection which stays open untill the token expires

import websocket

def on_message(ws, message):
    print ('message received ..')
    print(message)

def on_error(ws, error):
    print ('error happened .. ')
    print (error)

def on_close(ws):
    print ("The websocket has been closed!")

def on_open(ws):
    print('Congrats! the websocket is now open')

def func_websocket(ws):
    ws.run_forever()

Now we will create a Websocket App in the main function using the authentication token you have received previously. This Websocket App is a built in class in the websocket-client and will be used to call the functions that we defined before.

The url that is used to make the websocket connection in HIRO™ 7 is: wss://core.arago.co/api/action-ws/1/ and the SubProtocol is: action-1.0.0.

The authentication token will be passed as a part of the subprotocol in the header in the format "token-"<token>.

Lastly we will call the func_websocket function defined above to open the websocket connection with the server.

# Initiate session
hiro_session = initiate_hiro_session('./hiro_credentials_ah.json')
if hiro_session['active'] == True:
    print('continue using the API ...')
    ws_token = hiro_session['token']

    if __name__ == '__main__':
        ws_header = 'action-1.0.0, token-' + ws_token   #header with the subprotocol and tokem
        ws = websocket.WebSocketApp(
            'wss://core.arago.co/api/action-ws/1/',
            on_open=on_open,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
            header={'Sec-WebSocket-Protocol': ws_header}
        )
    socket = func_websocket(ws)   #function call to initiate the websocket connection

You should see this if the websocket is successfully opened:

Congrats! the websocket is now open

You can use the Trace function just before the websocket class to enable debugging:

web_trace = websocket.enableTrace(True)

Congrats, you have successfully opened your websocket with the server. As next steps, we will learn how to listen to the websocket for the different types of messages.

Listening to the websocket

Now we will modify the on_message function to listen to the websocket and respond according to the commands received. There can be two possible scenarios when we can receive a msg from the graph:

  1. The websocket sends a submitAction type of message in the format below:

{
    "id": "the $appId:$requestId in AH->Graph, the $requestId in Graph->App",
    "scope":"$scope of your instance from which the KI was triggered",
    "parameters":{
        "host":"sshtest.arago.de",
        "command":"hostname",
        "user":"centos",
        "timeout":60000000},
    "type":"submitAction",
    "capability":"ExecuteCommand",
    "timeout":60000000,
    "handler":"$id of the handler"}

In this case we will put the message in a global queue, so that it can be retrieved later in the main fucntion to extract the parameters and execute an SSH to the required hostname. We will then send a acknowledgement back to the server to imply that we have received the message with the action to be executed (otherwise the server will keep on sending the submitAction message untill the ack is received).

  1. The server sends an acknowledgement to the websocket after receiving the result. A sample ack message looks as follows:

{
    "type": "acknowledged",
    "id": "the $appId:$requestId in AH->Graph, the $requestId in Graph->App",
    "code": 200,
    "message": ""
}

In this case we will just catch the acknowledgement and print that the acknowledgement has been received.

So the modified on_message function we will have will be as follows:

import queue

msg_q = queue.Queue()  #global queue to access websocket messages

def on_message(ws, message):
    print ('message received ..')
    print(message)
    message_object = json.loads(message)
    message_type = message_object['type']
    if (message_type == 'submitAction'):      #if the message is a submitType action message
        message_id = message_object['id']
        print("message type is: " + message_type)
        print("message id is: " + message_id)

        ack = {
            "type": "acknowledged",
            "id": message_id,
            "code": 200,
            "message": "Received the action"
            }

        ack_msg = json.dumps(ack)
        ws.send(ack_msg)                #sending an ack back if action is received
        msg_q.put(message)

    if (message_type == 'acknowledged'):    #If the message sent by the server is an ack message
        print("result has been acknowledged")

Now that we have modified our on_message function to handle all the messages sent by the server, as next steps, we will now parse the submitAction message received from the server to extract the 'host' and 'user' parameters that we require to perform the SSH action.

Extracting parameters from the message received

To parse the message we will first need to run our websocket on a separate thread. This will allow us to parallelly extract the parameters and use them for execution without closing the websocket. For more information on how threading works in Python please refer to the library on Threading.

First we will define a function 'extract_parameters' that will extract the required parameters for us and return them to main:

def extract_parameters(message):
    message_object = json.loads(message)
    message_parameters = dict()
    message_parameters['id'] = message_object['id']
    message_parameters['hostname'] = message_object ['parameters']['host']
    message_parameters['user'] = message_object['parameters']['user']
    return message_parameters

Then to keep the websocket running and extract the parameters parallelly we will modify the main function to run the websocket on a thread, get the message from the queue, and then extract the required parameters into variables using the above 'extract_parameters' function.

The modified main will be:

import threading

# Initiate session
hiro_session = initiate_hiro_session('./hiro_credentials_ah.json')
if hiro_session['active'] == True:
    print('continue using the API ...')
    ws_token = hiro_session['token']

     if __name__ == '__main__':
        ws_header = 'action-1.0.0, token-' + ws_token   #header with the subprotocol and tokem
        ws = websocket.WebSocketApp(
            'wss://core.arago.co/api/action-ws/1/',
            on_open=on_open,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
            header={'Sec-WebSocket-Protocol': ws_header}
        )

    ws_thread= threading.Thread(target = func_websocket, args=[ws])     #Separate Thread to initiate the websocket, passing the websocket as a parameter
    ws_thread.start()
    while True:     #Main Thread

        msg = msg_q.get()
        print("message is : " + msg )

        message_parameters = extract_parameters(msg)
        message_id= message_parameters['id']
        print("The hostname is: " + message_parameters['hostname'])
        print("the user is: " + message_parameters['user'])

Congrats you have now successfully extracted and printed the hostname and username sent from the server. You can now use these functions to perform an SSH on the target hostname. As next steps we will learn how to send a result back to the server.

Sending the result back to the Action API

We will now define a function which will send the result back to the server. For the sake of simplicity we will assume that the SSH request performed on the target machine was successful, therefore, we will send a result with a successful message back to the server.

First we will define a function 'send_actionresult' which returns a result json to be sent in this format. The format of the json is as follows:

{
  "type": "sendActionResult",
  "id": "the $appId:$requestId in AH->Graph, the $requestId in Graph->App",
  "result": {
    "resultfield1": "resultvalue1",
    "resultfield2": "resultvalue2"
  }
}

The function can be defined as follows:

def send_actionresult(id):
    result =  {
        "type": "sendActionResult",
        "id": id,
        "result": "ssh was successful",
    }
    message_result = json.dumps(result)
    return message_result

For the sake of simplicity we are just sending a 'ssh was successful' msg without performing a real SSH using the parameters. The user can send the result according to the message he receives from the machine he is trying to SSH to and pass that as a parameter to this function.

We will also modify the main to send the result back to the server through the websocket. The modified function is:

# Initiate session
hiro_session = initiate_hiro_session('./hiro_credentials_ah.json')
if hiro_session['active'] == True:
    print('continue using the API ...')
    ws_token = hiro_session['token']

     if __name__ == '__main__':
        ws_header = 'action-1.0.0, token-' + ws_token   #header with the subprotocol and tokem
        ws = websocket.WebSocketApp(
            'wss://core.arago.co/api/action-ws/1/',
            on_open=on_open,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
            header={'Sec-WebSocket-Protocol': ws_header}
        )

    ws_thread= threading.Thread(target = func_websocket, args=[ws])
    ws_thread.start()
    while True:

        msg = msg_q.get()
        print("message is : " + msg )
        message_parameters = extract_parameters(msg)
        message_id= message_parameters['id']
        print("The hostname is: " + message_parameters['hostname'])
        print("the user is: " + message_parameters['user'])
        result = send_actionresult(message_id)
        print(result)
        ws.send(result)

Final Refactored Action Handler

We will now refactor the code to add a couple of things to enable ease of use. First, we have modified the authentication function to also return the action-ws (Action API) path (in the scenario the base instance URL changes the code will not be affected). We have then replaced the https:// of the url to wss:// to change it to a websocket URL. We have also closed the websocket after 60 seconds of activity using ws_close. You can remove this once you eploy it to live to enable a long lived connection. The final refactored code can be used as follows:

# Imports
import json
import pprint
import requests
import ssl
import websocket
import queue
import threading
import time

pp = pprint.PrettyPrinter(indent=4)

msg_q = queue.Queue()  #global queue to access websocket messages


# Define authentication function
def initiate_hiro_session(credentials_file_name):
    with open(credentials_file_name) as json_file:
        hiro_credentials = json.load(json_file)
    hiro_session_context = dict()
    hiro_api_base = hiro_credentials['api_url']
    # https://core.arago.co/help/
    hiro_session_context['versions'] = hiro_api_base + '/api/version'
    hiro_session_context['auth_api'] = hiro_api_base + '/api/auth/6/'
    hiro_session_context['graph_api'] = hiro_api_base + 'graph/7/'
    hiro_session_context['action_api'] = hiro_api_base + '/api/action/1/'
    hiro_session_context['action_ws_api'] = hiro_api_base + '/api/action-ws/1/'



# query_response = requests.get('https://ec2-63-33-203-84.eu-west-1.compute.amazonaws.com:8443/api/version', verify= False)
#
# pp.pprint(query_response.text)

#     # authenticate
    print('Authenticating with HIRO Auth Service to receive an auth token ...')
    auth_data = {'client_id': hiro_credentials['client_id'], 'client_secret': hiro_credentials['client_secret'],
                 'username': hiro_credentials['username'], 'password': hiro_credentials['password']}
    hiro_auth_api = str(hiro_session_context['auth_api'])
    print(hiro_auth_api)
    response = requests.post(hiro_auth_api + 'app', data=json.dumps(auth_data), verify=False)

    print('Response code: ' + str(response))
    if response.ok:
        #print('You have successfully created a HIRO access token and can start using the HIRO API.')
        hiro_user_token = response.json()['_TOKEN']
        print(hiro_user_token)
        hiro_session_context['token'] = hiro_user_token
        hiro_session_context['session'] = requests.Session()
        hiro_session_context['session'].headers.update({'Authorization': 'Bearer ' + hiro_user_token})
        hiro_session_context['active'] = True
    else:
        print('Authentication error: ' + str(response.status_code) + ' - ' + response.reason)
        print('Error message: ' + response.text)
        print('Please check your credentials and try again.')
        hiro_session_context['active'] = False

    return hiro_session_context

def on_message(ws, message):
    print ('message received ..')
    print(message)
    message_object = json.loads(message)
    message_type = message_object['type']
    if (message_type == 'submitAction'):
        message_id = message_object['id']
        print("message type is: " + message_type)
        print("message id is: " + message_id)

        ack = {
            "type": "acknowledged",
            "id": message_id,
            "code": 200,
            "message": "Received the action"
            }

        ack_msg = json.dumps(ack)
        ws.send(ack_msg)                #sending an ack back if action is received
        msg_q.put(message)

    if (message_type == 'acknowledged'):
        print("result has been acknowledged")



def on_error(ws, error):
    print ('error happened .. ')
    print (error)


def on_close(ws):
    print ("the websocket was closed")


def on_open(ws):
    print('Congrats! the websocket is now open')

def func_websocket(ws):
    ws.run_forever()

def extract_parameters(message):
    message_object = json.loads(message)
    message_parameters = dict()
    message_parameters['id'] = message_object['id']
    message_parameters['hostname'] = message_object ['parameters']['host']
    message_parameters['user'] = message_object['parameters']['user']
    return message_parameters

def send_actionresult(id):
    result =  {
        "type": "sendActionResult",
        "id": id,
        "result": "ssh was successful",
    }
    message_result = json.dumps(result)
    return message_result


# Initiate session
hiro_session = initiate_hiro_session('./hiro_credentials_ah.json')
if hiro_session['active'] == True:
    print('continue using the API ...')
    ws_token = hiro_session['token']

    if __name__ == '__main__':
        web_trace = websocket.enableTrace(False)
        https_url = hiro_session['action_ws_api']
        ws_url = https_url.replace("https", "wss")
        ws_header = '1.0, token-' + ws_token
        ws = websocket.WebSocketApp(
            ws_url,
            on_open=on_open,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
            header={'Sec-WebSocket-Protocol': ws_header}
        )

    ws_thread= threading.Thread(target = func_websocket, args=[ws])
    ws_thread.start()
    while True:

        msg = msg_q.get()
        print("message is : " + msg )

        message_parameters = extract_parameters(msg)
        message_id= message_parameters['id']
        print("The hostname is: " + message_parameters['hostname'])
        print("the user is: " + message_parameters['user'])
        result = send_actionresult(message_id)
        print(result)
        ws.send(result)
        time.sleep(60)
        ws.close()

Summary

Congrats you have successfully created a SSH Action Handler using the Action API.

We have performed the following steps for implementing this Action Handler:

  • Created a websocket connection with the Action API

  • Listened to the websocket connection for the action to be executed

  • Parsed the parameters so that they can be used by the user to execute an action

  • Sent the result back to Action API

Now that you have created your first Action Handler you can use this to SSH to a machine. You can also use this tutorial as a base to create another Action Handler with different capabilties.