Documentation

12.4. Python API Event Handler

12.4.1. Introduction

SFTPPlus allows developers to write custom event handlers using the Python programming language.

The handlers are execute in separate independent processes / CPU cores, without shared memory. This is why the handle() method of the extension needs to be a @staticmethod and always received the configuration.

The handler is initialized multiple time. One instance is created in the main process and extra instances are created for each CPU.

A single extension instance can have the onStart(configuration) / onStop() method called multiple times during its lifetime. onStart(configuration) and onStop() methods are only called for the instance running in the main process.

The code for the event handler needs to be placed in a Python file (module) inside the extension/ folder from the SFTPPlus installation folder.

You can find an extensive example inside the extension/demo_event_handler.py folder of the default SFTPPlus installation.

Below is an example extension code that is also used to document the available API and functionalities.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# Place this file in `extension/demo_event_handler.py`
# inside SFTPPlus' installation folder.
#
# For the event handler set
# type = extension
# entry_point = python:demo_event_handler.DemoEventHandler
#
from __future__ import unicode_literals
import json


class DemoEventHandler(object):
    """
    An event handler which just emits another event with details of the
    received events.

    Events handler API extensions can emit the events:

    * 20174 - Emitted by SFTPPlus for critical errors.
              Should not be explicitly emitted by the extension.
    * 20000 - For debug messages.
    * 20200 - For normal messages.

    This is also used as a documentation for the API and is included in
    the automated regression testing process.
    """

    def __init__(self):
        """
        Called when the event handler is initialized in each worker and
        in the main process.
        """
        self._configuration = None

    def onStart(self, parent):
        """
        Called in the main process when the event handler starts.

        `parent.configuration` is the Unicode string defined in the
        extension configuration option.

        Any exception raised here will stop the event handler from
        starting.
        """
        self._parent = parent
        self._configuration = parent.configuration

    def onStop(self):
        """
        Called in the main process when the event handler stops.
        """
        self._configuration = None

    def getConfiguration(self, event):
        """
        Called in the main process before dispatching the event to
        the worker process.

        It can be used as validation for the event,
        before handling the event in a separate CPU core.

        Return the configuration for the event as Unicode.

        Return `None` when the event handling should be skipped and the
        `handle` function will no longer be called for this emitted event.

        As advanced usage, it can return a `deferred` which will delay
        the execution of the event, without keeping a worker process busy.
        This mechanism can also be used for implementing a wait condition
        based on which the event is handled or not.
        """
        if event.id == '1234' or event.account.name == 'fail-user':
            # Any exception raised here will stop the handling of this
            # specific event instance by the extension.
            raise RuntimeError('Rejected event.')

        if event.account.name == 'skip-user':
            # When `None` is returned the handling is skipped and the
            # `handle` function will not be called.
            self._parent.emitEvent(
                '20200', data={'message': 'Handling skipped.'})
            return None

        if event.account.name == 'delay-user':
            # For username `delay-user` we delay processing of the event
            # for 0.5 seconds.
            return self._parent.delay(0.5, lambda: 'delayed-configuration')

        # Events can be emitted here via the parent.emitEvent API
        # to generate an event before the start of event processing.
        self._parent.emitEvent('20000', data={'message': 'Handling starts.'})

        return self._configuration

    @staticmethod
    def handle(event, configuration):
        """
        Called in a separate process when it should handle the event.

        This is a static function that must work even when
        onStart and onStop were not called.

        `configuration` is the Unicode value returned by
        getConfiguration(event).

        If an exception is raised the processing is stopped for this event.
        Future events will continue to be processed.
        """
        # Output will overlap with the output from other events as each
        # event is handled in a separate thread.

        if event.account.name == 'inactive-user':
            # The extension can return a text that is logged as an event.
            return 'Extension is not active from this user.'

        if event.account.name == 'ignored-user':
            # Don't handle events from a certain username.
            # The extension can return without any value, and no
            # event is emitted.
            return

        # Here we get the full event, and then we sample a few fields.
        message = (
            'Received new event for DemoEventHandler\n'
            '{event_json}\n'
            '-----------------\n'
            'configuration: {configuration}\n'
            '-----------------\n'
            'id: {event.id}\n'
            'account: {event.account.name}\n'
            'at: {event.timestamp.timestamp:f}\n'
            'from: {event.component.name}\n'
            'data: {event_data_json}\n'
            '---\n'
            )
        output = message.format(
            event=event,
            event_json=json.dumps(event, indent=2),
            event_data_json=json.dumps(event.data, indent=2),
            configuration=configuration,
            )

        # Inform the handler to emit several events at the end.
        # For a single event, it is recommended to pass only a dictionary.
        return [
            # The "message" attribute is required.
            {'message': 'A simple message.'},
            # Other attributes are allowed.
            {'message': 'state', 'value': 'OK'},
            # Explicit Event ID is also supported
            # For this case the attributes should match the attributes
            # required by the requested Event ID.
            ('20000', {'message': output, 'configuration': configuration})
            ]

This event handler can be configured as:

[event-handlers/56df1d0a-78c6-11e9-a2ff-137be4dbb9a8]
enabled = yes
type = extension
name = python-extension

entry_point = python:extensions.demo_event_handler.DemoEventHandler

configuration = some-free-text-configuration

12.4.2. Execution queue

SFTPPlus will only handle in parallel N events, where N is based on the number of CPUs available to the OS.

All the other events required to be handled by the extensions are placed into a queue.

The extension is called to handle the event only when there are free CPUs.

To prevent misconfiguration, there is a hard limit of 10 minutes for how long an event can stay in the queue and for processing the event.

12.4.3. Event data members

The event object received in the handler has the following structure.

The overall structure of the event object is presented below.

The following variables (case-insensitive) are provided as context data containing information about the event being triggered:

  • id

  • uuid

  • message

  • account.name

  • account.peer.address

  • account.peer.port

  • account.peer.protocol

  • account.peer.family

  • account.uuid

  • component.name

  • component.type

  • component.uuid

  • timestamp.cwa_14051

  • timestamp.iso_8601

  • timestamp.iso_8601_fractional

  • timestamp.iso_8601_local

  • timestamp.iso_8601_basic

  • timestamp.iso_8601_compact

  • timestamp.timestamp

  • server.name

  • server.uuid

  • data.DATA_MEMBER_NAME

  • data_json

The members of data are specific to each event. See Events page for more details regarding the data available for each event.

Many events have data.path and data.real_path, together with the associated data.file_name, data.directory_name, data.real_file_name, and data.real_directory_name.

Below is the description for the main members of the event object.


name

id

type

string

optional

No

description

ID of this event. See Events page for the list of all available events.


name

message

type

string

optional

No

description

A human readable description of this event.


The timestmap contains the following attributes:


name

timestamp

type

string

optional

No

description

Date and time at which this event was created, as Unix timestamp with milliseconds.


name

cwa_14051

type

string

optional

No

description

Date and time in CWA 14051 at which this event was emitted.


The account contains the following attributes:


name

uuid

type

string

optional

No

description

UUID of the account emitting this event. In case no account is associated with the event, this will be the special process account. In case the associated account is not yet authenticated this will be the special peer account.


name

name

type

string

optional

No

description

Name of the account emitting this event.


name

peer

type

JSON Object

optional

No

description

Address of the peer attached to this account. This might be a local or remote address, depending on whether the account is used for client side or server side interaction.


The peer contains the following attributes:


name

address

type

string

optional

No

description

IP address of this connection.


name

port

type

integer

optional

No

description

Port number of this connection.


name

protocol

type

string

optional

No

description

OSI Layer 4 transport layer protocol used for this connection in the form of either TCP or UDP.


The component contains the following attributes:


name

uuid

type

string

optional

No

description

UUID of the component (service or transfer) emitting this event.


name

type

type

string

optional

No

description

Type of the component emitting this event.


The server contains the following attributes:


name

uuid

type

string

optional

No

description

UUID of the server emitting this event.


name

type

type

string

optional

No

description

Type of the server emitting this event.