Plugins
Due to customer modifications, the UCS server allows Python scripts to run as part of the main one process.
Running (v2.4.0)
- When UCS starts, the
/opt/ucs/server/plugins
directory is searched and processed all files with.py
extension (except the__init__.py
file). - The file is imported and the variable
UCS_PLUGIN
is searched for. If the variable is not in the file, then it is skipped. - According to the string content of the
UCS_PLUGIN
variable, UCS will try to create an instance given class. Theucs
argument is passed to the constructor, which is the server's main class. - If the class contains a
start
method, then this method is called. This method must not be blocking because plugins are not automatically created as threads. - When reloading the plugin or stopping UCS, the
stop
method is called if the plugin eats contains.
#!/usr/bin/python3
"""Example plugin"""
import threading
from constants import AGENT_AUX
from events import EVENT_AGENT_STATUS
UCS_PLUGIN = 'Example'
class Example(threading.Thread):
"""Example plugin"""
def __init__(self, ucs):
super().__init__(self)
self.name = 'ExamplePluginThread'
self.ucs = ucs
self.ucs.addCallback(EVENT_AGENT_STATUS, self.agent_status_changed)
self.terminate = False
self.work = threading.Event()
def agent_status_changed(self, agent, args):
"""Called when agent changes status"""
if agent['status'] == AGENT_AUX and agent['reason'] == 'Lunch':
self.ucs.log.info(f'Agent {agent["displayname"]} left for lunch')
def run(self):
"""Thread main loop"""
while True:
self.work.wait(60)
self.work.clear()
if self.terminate:
return
agents = self.ucs.agents.getAgents()
for agent in agents:
if agent['status'] == AGENT_AUX and agent['reason'] == 'Lunch':
self.ucs.log.info(f'Agent {agent["displayname"]} is at lunch')
def stop(self):
"""Called on plugin reload or UCS stop"""
self.ucs.removeCallback(EVENT_AGENT_STATUS, self.agent_status_changed)
self.terminate = True
self.work.set()
API Methods (v2.4.0)
Plugins can be uploaded and removed while UCS is running (superuser permission is required). The following API methods are used for this:
ucs.api.plugins.load(sid, plugin_name)
loads the plugin or if the plugin already exists, then stops it and uploads it again.ucs.api.plugins.unload(sid, plugin_name)
stops the plugin.
Attention, if the plugin registers callbacks, callbacks are required in the stop method unregister. Otherwise, they will remain referenced and will continue to be invoked.
Agent state change hook (v2.4.0)
The plugin can affect the agent state change. UCS provides a method for registration
callback ucs.agents.addChangeStatusHook(callback)
and to deregister it
ucs.agents.removeChangeStatusHook()
when stopping the plugin. He can use this method
only one plugin, if another plugin tries to register then it is invoked
ProcessingError
exception.
Three arguments are passed to the callback:
- agent:agents/Agent is an instance of the agent class at the moment before the state change.
- status:int is the new status (constant
AGENT_STATE_ENUM
). - reason:str is the reason for the not ready status.
The callback must return status:int, reason:str or throw a ProcessingError
exception.
- If the state to which the agent is switched should not be affected, then callback
returns the values of the
status
andreason
arguments as passed to it. - If there is to be a switch to another state and/or reason for not being ready, then returns the values to which the agent should be switched.
- If the switch is to be blocked by an error, then the plugin can either return the first argument
with the value None and the second argument with the blocking reason. If there is no reason to block
passed (
bool(reason) is False
) then Blocked by hook is set. This will cause throwing aProcessingError(reason)
exception. Alternatively, this exception can be raised directly from the callback. - If an exception other than
ProcessingError
occurs during processing in the callback, then the agent is switched to the originally requested state and the backtrace is logged.
"""Agent status change hook example"""
import time
from constants import AGENT_READY, AGENT_AUX
from error import ProcessingError
UCS_PLUGIN = 'StatusHook'
class StatusHook:
"""Agent status change hook example"""
def __init__(self, ucs):
self.ucs = ucs
self.ucs.agents.addChangeStatusHook(self.hook)
def stop(self):
"""Called on plugin reload or UCS stop"""
self.ucs.agents.removeChangeStatusHook()
def hook(self, agent, new_status, new_reason):
"""Called when agent status is about to change"""
hour = time.localtime().tm_hour
if new_status == AGENT_AUX and new_reason == 'Lunch':
if hour < 11:
return None, 'It's too early for lunch'
if hour > 17:
raise ProcessingError('It's time for dinner')
if agent.status == AGENT_OFFLINE and new_status == AGENT_READY:
return AGENT_AUX, 'Getting to know the operating instructions'
return new_status, new_reason
Shared data change hook (v3.1.0)
The plugin can modify the set data into shared data. UCS provides a method
to register the callback ucs.shared.addChangeHook(identifier, callback)
and for
its deregistration ucs.agents.removeChangeHook(identifier)
when stopping the plugin.
Only one plugin can use this method for a given identifier, if o
another plugin attempts to register, then a ProcessingError
exception is thrown.
Three arguments are passed to the callback:
- identifier:str is the identifier of the changed shared data.
- old_data:any is the original data.
- new_data: any are newly set data.
The callback can return arbitrary data or throw a ProcessingError
exception if
the data change is to be blocked. If another exception occurs during processing in the callback
than ProcessingError
, then the data is updated as requested and is logged
backtrace except in callback.
"""Shared data change hook example"""
from error import ProcessingError
UCS_PLUGIN = 'SharedDataHook'
class SharedDataHook:
"""Shared data change hook example"""
def __init__(self, ucs):
self.ucs = ucs
self.ucs.shared.addChangeHook('agent_emails', self.hook)
def stop(self):
"""Called on plugin reload or UCS stop"""
self.ucs.shared.removeChangeHook('agent_emails')
def hook(self, identifier, old_data, new_data):
"""Called when new shared data are about to change"""
if not isinstance(new_data, dict):
raise ProcessingError('Only associative array is accepted')
data = {}
username2id = {
username: user_id
for user_id, username
in self.ucs.tree.getUsers('username').items()
}
for username, count in new_data.items():
user_id = username2id.get(username)
if user_id:
data[user_id] = count
old_count = old_data.get(user_id)
if old_count is not None:
self.ucs.log.dbug('User ID %d email count changed from %s to %s', user_id, old_count, count)
otherwise:
self.ucs.log.warn('Unknown username: %s', username)
return data
API middleware (v3.8.0)
Individual API requests can be handled using middleware. UCS provides a method
for middleware registration ucs.api.addMiddleware(method, middleware)
and for
its deregistration ucs.api.removeMiddleware(method, middleware)
when stopping the plugin.
Multiple middlewares can be registered for one API method. Middleware when calling
gets a handler
as its first argument (which is the API method itself or another
middleware) which is followed by arguments passed as API call parameters. Middleware
can modify the input arguments, then must call the passed handler or
throw an exception. Returns the result of a built-in UCS API method, which it can modify.
"""Middleware plugin example"""
from error import InvalidDataInput, PermissionDenied
UCS_PLUGIN = "Middleware"
class Middleware:
"""Middleware plugin example"""
def __init__(self, ucs):
self.ucs = ucs
self.ucs.api.addMiddleware("config.users.get", self.get_user)
self.ucs.api.addMiddleware("config.users.edit", self.edit_user)
self.ucs.api.addMiddleware("config.users.delete", self.delete_user)
def stop(self):
"""Cleanup on UCS shutdown"""
self.ucs.api.removeMiddleware("config.users.get", self.get_user)
self.ucs.api.removeMiddleware("config.users.edit", self.edit_user)
self.ucs.api.removeMiddleware("config.users.delete", self.delete_user)
def get_user(self, handler, *args):
"""Just log middleware usage"""
self.ucs.log.dev("Middleware get_user args: %s", args)
result = handler(*args)
self.ucs.log.dev("Middleware get_user result: %s", result)
return result
def edit_user(self, handler, user, data):
"""Keep username and password"""
if data["id"] == 1:
if "username" in data and data["username"] != "admin":
raise InvalidDataInput({"username": "Admin username can't be changed"})
if "password" in data and data["password"] and len(data["password"]) < 10:
raise InvalidDataInput({"password": "Admin password is too short"})
return handler(user, data)
def delete_user(self, *args):
"""Disable user deletion"""
raise PermissionDenied("This UCS forbids user removal")