Clients
Description
A client plugin is simply a GenericClient
subclass designed to implement simple events handlers, and to split their tasks in atomic subtasks to ensure consistent error reprocessing.
Requirements
Here is a commented minimal plugin implementation that won’t do anything, as it doesn’t implement any event handlers yet.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Required to subclass GenericClient
from clients import GenericClient
# Required for event handlers method type hints
from lib.config import HermesConfig # only if the plugin implement an __init__() method
from lib.datamodel.dataobject import DataObject
from typing import Any
# Required to indicate to hermes which class it has to instantiate
HERMES_PLUGIN_CLASSNAME = "MyPluginClassName"
class MyPluginClassName(GenericClient):
def __init__(self, config: HermesConfig):
# The 'config' var must not be used nor modified by the plugin
super().__init__(config)
# ... plugin init code
Handlers methods
Event handlers
For each data type set up in the client datamodel, the plugin may implement a handler for each of the 5 possible event types:
added
: when an object is addedrecycled
: when an object is restored from trashbin (will never be called if trashbin is disabled)modified
: when an object is modifiedtrashed
: when an object is put in trashbin (will never be called if trashbin is disabled)removed
: when an object is deleted
If an event is received by a client, but its handler isn’t implemented, it will silently be ignored.
Each handler must be named on_datatypename_eventtypename.
Example for a Mydatatype
data type:
def on_Mydatatype_added(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
newobj: DataObject,
):
pass
def on_Mydatatype_recycled(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
newobj: DataObject,
):
pass
def on_Mydatatype_modified(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
newobj: DataObject,
cachedobj: DataObject,
):
pass
def on_Mydatatype_trashed(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
cachedobj: DataObject,
):
pass
def on_Mydatatype_removed(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
cachedobj: DataObject,
):
pass
Event handlers arguments
-
objkey
: the primary key of the object affected by the event -
eventattrs
: a dictionary containing the new object attributes. Its content depends upon the event type:added
/recycled
events: contains all object attributes names as key, and their respective values as valuemodified
event: always contains three keys:added
: attributes that were previously unset, but now have a value. Attribute names as key, and their respective values as valuemodified
: attributes that were previously set, but whose value has changed. Attribute names as key, and their respective new values as valueremoved
: attributes that were previously set, but now don’t have a value anymore. Attribute names as key, andNone
as value
trashed
/removed
events: always an empty dict{}
-
newobj
: aDataObject
instance containing all the updated values of the object affected by the event (see DataObject instances below) -
cachedobj
: aDataObject
instance containing all the previous (cached) values of the object affected by the event (see DataObject instances below)
DataObject instances
Each data type object can be used intuitively through a DataObject
instance.
Let’s use a simple example with this User
object values (without a mail) from datamodel below:
{
"user_pkey": 42,
"uid": "jdoe",
"givenname": "John",
"sn": "Doe"
}
hermes-client:
datamodel:
Users:
hermesType: SRVUsers
attrsmapping:
user_pkey: srv_user_id
uid: srv_login
givenname: srv_firstname
sn: srv_lastname
mail: srv_mail
Now, if this object is stored in a newobj
DataObject instance:
>>> newobj.getPKey()
42
>>> newobj.user_pkey
42
>>> newobj.uid
'jdoe'
>>> newobj.givenname
'John'
>>> newobj.sn
'Doe'
>>> newobj.mail
AttributeError: 'Users' object has no attribute 'mail'
>>> hasattr(newobj, 'sn')
True
>>> hasattr(newobj, 'mail')
False
Error handling
Any unhandled exception raised in an event handler will be managed by GenericClient
, that will append the event to its error queue.
GenericClient
will then try to process the event regularly until it succeeds, and therefore call the event handler.
But sometimes, a handler must process several operations on target. Imagine a handler like this:
def on_Mydatatype_added(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
newobj: DataObject,
):
if condition:
operation1() # condition is False, operation1() is not called
operation2() # no error occurs
operation3() # this one raises an exception
At each retry the operation2()
function will be called again, but this is not necessarily desirable.
It is possible to divide a handler in steps by using the currentStep
attribute inherited from GenericClient
, to resume the retries at the failed step.
currentStep
always starts at 0 on normal event processing. Its new values are then up to plugin implementations.
When an error occurs, the currentStep
value is stored in the error queue with the event.
The error queue retries will always restore the currentStep
value before calling the event handler.
So by implementing it like below, operation2()
will only be called once.
def on_Mydatatype_added(
self,
objkey: Any,
eventattrs: "dict[str, Any]",
newobj: DataObject,
):
if self.currentStep == 0:
if condition:
operation1() # condition is False, operation1() is not called
# Declare that changes have been propagated on target
self.isPartiallyProcessed = True
self.currentStep += 1
if self.currentStep == 1:
operation2() # no error occurs
# Declare that changes have been propagated on target
self.isPartiallyProcessed = True
self.currentStep += 1
if self.currentStep == 2:
operation3() # this one raises an exception
# Declare that changes have been propagated on target
self.isPartiallyProcessed = True
self.currentStep += 1
Understanding isPartiallyProcessed
attribute
The isPartiallyProcessed
attribute inherited from GenericClient
indicates if the current event processing has already propagated some changes on target. Therefore, it must be set to True
as soon as the slightest modification has been propagated to the target.
It allows autoremediation to merge events whose currentStep
is different from 0 but whose previous steps have not modified anything on the target.
isPartiallyProcessed
is always False
on normal event processing. Its value change is up to plugin implementations.
With the implementation example above, and an exception raised by operation3()
, the autoremediation would not try to merge this partially processed event with possible subsequent events, as isPartiallyProcessed
is True
.
With the implementation example above, but an exception raised by operation2()
, the autoremediation would try to merge this unprocessed event with possible subsequent events, as isPartiallyProcessed
is still False
.
on_save handler
A special handler may be implemented when hermes just have saved its cache files: once some events have been processed and no event is waiting on the message bus, or before ending.
As this handler isn’t a standard event handler, GenericClient
can’t handle exceptions for it, and process to a retry later.
Any unhandled exception raised in this handler will immediately terminate the client.
It’s up to the implementation to avoid errors.
def on_save(self):
pass
GenericClient properties and methods
Properties
-
currentStep: int
Step number of current event processed. Allow clients to resume an event where it has failed.
-
isPartiallyProcessed: bool
Indicates if the current event processing has already propagated some changes on target.
Must be set toTrue
as soon as the slightest modification has been propagated to the target.
It allows autoremediation to merge events whosecurrentStep
is different from 0 but whose previous steps have not modified anything on the target. -
isAnErrorRetry: bool
Read-only attribute that can let client plugin handler know if the current event is being processed as part of an error retry. This can be useful for example to perform additional checks when a library happens to throw exceptions even though it has correctly processed the requested changes, as python-ldap sometimes does.
-
config: dict[str, Any]
Dict containing the client plugin configuration.
Methods
-
def getDataobjectlistFromCache(objtype: str) -> DataObjectList
Returns cache of specified objtype, by reference. Raise
IndexError
ifobjtype
is invalidWarningAny modification of the cache content will mess up your client!!!
-
def getObjectFromCache(objtype: str, objpkey: Any ) -> DataObject
Returns a deepcopy of an object from cache. Raise
IndexError
ifobjtype
is invalid, or ifobjpkey
is not found -
def mainLoop() -> None
Client main loop
WarningCalled by Hermes, to start the client. Must never be called nor overridden