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.
Most custom extension are created to only handle a few event IDs. You can set the list of event IDs that can be handled by your extension via the TARGET_EVENTS instance member. The TARGET_EVENTS is accessed after onStart(configuration).
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# Place this file in `extension/demo_event_handler.py`
2# inside SFTPPlus' installation folder.
3#
4# For the event handler set
5# type = extension
6# entry_point = python:demo_event_handler.DemoEventHandler
7#
8import json
9
10
11class DemoEventHandler(object):
12 """
13 An event handler which just emits another event with details of the
14 received events.
15
16 Events handler API extensions can emit the events:
17
18 * 20174 - Emitted by SFTPPlus for critical errors.
19 Should not be explicitly emitted by the extension.
20 * 20000 - For debug messages.
21 * 20200 - For normal messages.
22
23 This is also used as a documentation for the API and is included in
24 the automated regression testing process.
25
26 TARGET_EVENTS defined the events for which this handler will be triggered.
27 """
28
29 TARGET_EVENTS = [
30 '99', '100', '102', # Custom event ID.
31 '20156', # Component started.
32 '20157', # Component stopped.
33 ]
34
35 def __init__(self):
36 """
37 Called when the event handler is initialized in each worker and
38 in the main process.
39 """
40 self._configuration = None
41
42 def onStart(self, parent):
43 """
44 Called in the main process when the event handler starts.
45
46 `parent.configuration` is the Unicode string defined in the
47 extension configuration option.
48
49 Any exception raised here will stop the event handler from
50 starting.
51 """
52 self._parent = parent
53 self._configuration = parent.configuration
54
55 def onStop(self):
56 """
57 Called in the main process when the event handler stops.
58 """
59 self._configuration = None
60
61 def getConfiguration(self, event):
62 """
63 Called in the main process before dispatching the event to
64 the worker process.
65
66 It can be used as validation for the event,
67 before handling the event in a separate CPU core.
68
69 Return the configuration for the event as Unicode.
70
71 Return `None` when the event handling should be skipped and the
72 `handle` function will no longer be called for this emitted event.
73
74 As advanced usage, it can return a `deferred` which will delay
75 the execution of the event, without keeping a worker process busy.
76 This mechanism can also be used for implementing a wait condition
77 based on which the event is handled or not.
78 """
79 if event.id == '1234' or event.account.name == 'fail-user':
80 # Any exception raised here will stop the handling of this
81 # specific event instance by the extension.
82 raise RuntimeError('Rejected event.')
83
84 if event.account.name == 'skip-user':
85 # When `None` is returned the handling is skipped and the
86 # `handle` function will not be called.
87 return None
88
89 if event.account.name == 'skip-emit':
90 # When skipping, you can trigger emitting an event with custom
91 # message and attached data.
92 return None, {'message': 'Handling skipped.', 'extra': 'skip-emit'}
93
94 if event.account.name == 'error-user':
95 # You can skip and emit an event ID dedicated to errors.
96 return None, {
97 'event_id': '20202',
98 'message': 'Can be a generic description for the error case.',
99 'details': (
100 'Can contain details specific to this error. '
101 'Example a path to a file.'
102 ),
103 'tb': 'Can include option traceback info as text.',
104 }
105
106 if event.account.name == 'delay-user':
107 # For username `delay-user` we delay processing of the event
108 # for 0.5 seconds.
109 return self._parent.delay(0.5, lambda: 'delayed-configuration')
110
111 # Events can be triggered as part of the event handling configuration.
112 # You can have one for more events.
113 # Event can have custom ID or use default ID.
114 events = [
115 {'event_id': '20201', 'message': 'Handling started.'},
116 {'message': 'Default ID is 20200 as informational.'},
117 ]
118 # There is also the option of returning just the configuration,
119 # without any extra events.
120 return self._configuration, events
121
122 @staticmethod
123 def handle(event, configuration):
124 """
125 Called in a separate process when it should handle the event.
126
127 This is a static function that must work even when
128 onStart and onStop were not called.
129
130 `configuration` is the Unicode value returned by
131 getConfiguration(event).
132
133 If an exception is raised the processing is stopped for this event.
134 Future events will continue to be processed.
135 """
136 # Output will overlap with the output from other events as each
137 # event is handled in a separate thread.
138
139 if event.account.name == 'inactive-user':
140 # The extension can return a text that is logged as an event.
141 return 'Extension is not active from this user.'
142
143 if event.account.name == 'test@proatria.onmicrosoft.com':
144 # The extension has access to the Entra ID OAuth2 token.
145 return 'Entra ID token: {}'.format(event.account.token)
146
147 if event.account.name == 'ignored-user':
148 # Don't handle events from a certain username.
149 # The extension can return without any value, and no
150 # event is emitted.
151 return
152
153 # Here we get the full event, and then we sample a few fields.
154 message = (
155 'Received new event for DemoEventHandler\n'
156 '{event_json}\n'
157 '-----------------\n'
158 'configuration: {configuration}\n'
159 '-----------------\n'
160 'id: {event.id}\n'
161 'account: {event.account.name}\n'
162 'at: {event.timestamp.timestamp:f}\n'
163 'from: {event.component.name}\n'
164 'data: {event_data_json}\n'
165 '---\n'
166 )
167 output = message.format(
168 event=event,
169 event_json=json.dumps(event, indent=2),
170 event_data_json=json.dumps(event.data, indent=2),
171 configuration=configuration,
172 )
173
174 # Inform the handler to emit several events at the end.
175 # For a single event, it is recommended to pass only a dictionary.
176 return [
177 # The "message" attribute is required.
178 {'message': 'A simple message.'},
179 # Other attributes are allowed.
180 {'message': 'state', 'value': 'OK'},
181 # Explicit Event ID is also supported
182 # For this case the attributes should match the attributes
183 # required by the requested Event ID.
184 # Event '20201' requires the `message` attribute.
185 # Any extra attributes are allowed.
186 {'event_id': '20201', 'message': output, 'extra': configuration},
187 ]
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.email
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:
email
- type:
string
- optional:
yes
- description:
The primary email, as text associated to this account.
- name:
emails
- type:
string
- optional:
yes
- description:
A list of 2 value tuples (name, email) for the emails associated to this account.
- name:
token
- type:
string
- optional:
Yes
- description:
For Windows local or domain accounts a token that can be use to impersonate the account. For Azure AD accounts, when extra api_scopes are configured, this is the latest OAuth2 token that can be use to obtain access to an extra API or refresh a token.
- 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.