You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
shepherd-agent/shepherd/plugin.py

248 lines
8.2 KiB

#!/usr/bin/env python3
from contextlib import suppress
from abc import ABC, abstractmethod
import importlib
from types import SimpleNamespace
from collections import namedtuple
import sys
import os
import shepherd.scheduler
class Hook():
def __init__(self):
self.attached_functions = []
def attach(self, new_func):
if not callable(new_func):
raise TypeError("Argument to Hook.attach must be callable")
self.attached_functions.append(new_func)
def __call__(self, *args, **kwargs):
for func in self.attached_functions:
func(*args, **kwargs)
class InterfaceFunction():
def __init__(self, func):
if not callable(func):
raise TypeError("Argument to InterfaceFunction must be callable")
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class Plugin(ABC):
@staticmethod
@abstractmethod
def define_config(confdef):
pass
@abstractmethod
def __init__(self, plugininterface, config):
pass
def run(self, hooks, plugins):
pass
class SimplePlugin(Plugin):
@staticmethod
def define_config(confdef):
confdef.add_def()
def __init__(self, plugininterface, config):
super().__init__(plugininterface, config)
self.config = config
self.interface = plugininterface
self.plugins = plugininterface.other_plugins
self.hooks = plugininterface.hooks
plugin_interfaces = {} # dict of plugin interfaces
# convenience dicts bundling together lists from interfaces
plugin_functions = {} # dict of plugins containing callable interface functions
plugin_hooks = {} # dict of plugins containing hook namespaces
_defer = True
_deferred_attachments = []
_deferred_jobs = []
def init_plugins(plugin_classes, plugin_configs, core_config):
# Startup pluginmanagers
global plugin_interfaces
global plugin_functions
global plugin_hooks
global _defer
global _deferred_attachments
global _deferred_jobs
for name, plugin_class in plugin_classes.items():
# Instanciate the plugin interface - this also instanciates
# the plugin
plugin_interfaces[name] = PluginInterface(
name, plugin_class, plugin_configs[name], core_config)
plugin_functions[name] = plugin_interfaces[name].functions
plugin_hooks[name] = plugin_interfaces[name].hooks
# interfaces and hooks should now be populated, attach hooks, schedule jobs
_defer = False
for attachment in _deferred_attachments:
_attach_hook(attachment)
for job_desc in _deferred_jobs:
_add_job(job_desc)
# Hand shared interface callables back out
for plugininterface in plugin_interfaces.values():
plugininterface.functions = plugin_functions
def _add_job(job_desc):
global _deferred_jobs
global _defer
if not _defer:
shepherd.scheduler.schedule_job(job_desc)
else:
_deferred_jobs.append(job_desc)
def _attach_hook(attachment):
global plugin_hooks
global _deferred_attachments
global _defer
if not _defer:
targetplugin_hooks = plugin_hooks.get(attachment.pluginname)
if targetplugin_hooks is not None:
targethook = getattr(targetplugin_hooks, attachment.hookname)
if targethook is not None:
targethook.attach(attachment.func)
else:
raise Exception("Could not find hook '" +
attachment.hookname+"' in module '"+attachment.pluginname+"'")
else:
raise Exception(
"Cannot attach hook to non-existing module '"+attachment.pluginname+"'")
else:
_deferred_attachments.append(attachment)
# Eventually, would like to be able to have client plugin simply:
# self.shepherd.add_job(trigger, self.interface.myfunc)
# self.shepherd.attach_hook(pluginnanme,hookname, callable)
# self.shepherd.addinterface
# self.shepherd.hooks.myhook()
# self.shepherd.plugins.otherplugin.otherinterface()
# self.shepherd.add_job()
# Would be good to be able to use abstract methods to enable simpler plugin config
# defs. A way to avoid instantiating the class would be to run it all as class methods,
# enabling
HookAttachment = namedtuple(
'HookAttachment', ['pluginname', 'hookname', 'func'])
class PluginInterface():
# Class to handle the management of a single plugin.
# All interaction to or from the plugin to other Shepherd components or
# plugins should go through here.
def __init__(self, pluginname, pluginclass, pluginconfig, coreconfig):
if not issubclass(pluginclass, Plugin):
raise TypeError(
"Argument must be subclass of shepherd.plugin.Plugin")
self.coreconfig = coreconfig
self.hooks = SimpleNamespace() # My hooks
self.functions = SimpleNamespace() # My callable interface functions
self._name = pluginname
self._plugin = pluginclass(self, pluginconfig)
def register_hook(self, name):
setattr(self.hooks, name, Hook())
def register_function(self, func):
setattr(self.functions, func.__name__, InterfaceFunction(func))
@property
def other_plugins(self):
global plugin_functions
return plugin_functions
def attach_hook(self, pluginname, hookname, func):
_attach_hook(HookAttachment(pluginname, hookname, func))
# Add a job to the scheduler. By default each will be identified by the interface
# callable name, and so adding another job with the same callable will oevrride the first.
# Use the optional job_name to differentiate jobs with an extra string
def add_job(self, func, hour, minute, second=0, job_name=""):
for function_name, function in self.functions.__dict__.items():
if func == function.func:
# jobstring should canonically describe this job, to be retrieved
# after reboot later. Of the form:
# shepherd:pluginname:functionname:jobname
jobstring = "shepherd:"+self._name+":"+function_name+":"+job_name
_add_job(shepherd.scheduler.JobDescription(jobstring, hour, minute, second))
break
else:
raise Exception(
"Could not add job. Callable must first be registered as "
"a plugin interface with PluginInterface.register_function()")
"""
An interface to a Shepherd module, accessible by other modules.
All public methods in a module interface need to be threadsafe, as they will
be called by other modules (which generally run in a seperate thread)
"""
def find_plugins(plugin_names, plugin_dir=None):
plugin_classes = {}
for plugin_name in plugin_names:
# First look for core plugins, then the plugin_dir, then in the general import path
# for custom ones prefixed with "shepherd_"
try:
#mod = importlib.import_module("shepherd.plugins." + plugin_name)
mod = importlib.import_module('.'+plugin_name, "shepherd.plugins")
except ModuleNotFoundError:
try:
if (plugin_dir is not None) and (plugin_dir != ""):
if os.path.isdir(plugin_dir):
sys.path.append(plugin_dir)
mod = importlib.import_module("shepherd_" + plugin_name)
sys.path.remove(plugin_dir)
else:
raise Exception("plugin_dir is not a valid directory")
else:
mod = importlib.import_module("shepherd_" + plugin_name)
except ModuleNotFoundError:
raise Exception("Could not find plugin "+plugin_name)
# Scan imported module for Plugin subclass
attrs = [getattr(mod, name) for name in dir(mod)]
for attr in attrs:
with suppress(TypeError):
if issubclass(attr, Plugin):
plugin_classes[plugin_name] = attr
break
else:
raise Exception("Imported shepherd plugin modules must contain a "
"subclass of shepherd.plugin.Plugin, such as"
"shepherd.plugin.SimplePlugin")
return plugin_classes