From ebf225553daae7a6f2f75780d58092eefcc69bf3 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Wed, 28 May 2014 22:00:11 +1000 Subject: [PATCH] Add support for plugin actions implements blueprint mistral-pluggable-task-actions Change-Id: If12a0c6835edcabd33027555501dea4f473fc1f5 --- doc/source/writing_a_plugin_action.rst | 49 ++++++++++++++++++++++++++ etc/mistral.conf.example | 4 +++ mistral/actions/action_factory.py | 28 ++++++--------- mistral/actions/base.py | 34 +++++++++--------- mistral/config.py | 6 ++++ requirements.txt | 1 + setup.cfg | 7 ++++ 7 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 doc/source/writing_a_plugin_action.rst diff --git a/doc/source/writing_a_plugin_action.rst b/doc/source/writing_a_plugin_action.rst new file mode 100644 index 000000000..83f8ffcbe --- /dev/null +++ b/doc/source/writing_a_plugin_action.rst @@ -0,0 +1,49 @@ +How to write an Action Plugin +============================= + +1. Write a class based on mistral.actions.base.Actions +:: + + from mistral.actions import base + + class RunnerAction(base.Action): + def __init__(self, param): + # store the incomming params + self.param = param + + def run(self): + # return your results here + return {'status': 0} + +2. Publish the class in a namespace + (in your setup.cfg) + +:: + + [entry_points] + myproject.plugins.example = + runner = solum.mistral_plugins.somefile:RunnerAction + +3. Add the namespace into /etc/mistral/mistral.conf + (don't overwrite "mistral.plugins.std") + +:: + + action_plugins = mistral.plugins.std,myproject.plugins.example + +4. Use your plugin + +Note on naming the plugin. + + * The namespace is "myproject.plugins.example" + * The class is named "runner" + * Now you can call the action "example.runner" + +:: + + Workflow: + tasks: + myaction: + action: example.runner + parameters: + param: avalue_to_pass_in diff --git a/etc/mistral.conf.example b/etc/mistral.conf.example index 547d6cb86..493dc2600 100644 --- a/etc/mistral.conf.example +++ b/etc/mistral.conf.example @@ -30,6 +30,10 @@ rabbit_password = guest # Example: server=api,engine #server=all +# List of python module namespaces to search for plug-ins. +# See: doc/source/writing_a_plugin_action.rst on how to write a plugin. +action_plugins = mistral.actions.std + [api] # Host and port to bind the API server to host = 0.0.0.0 diff --git a/mistral/actions/action_factory.py b/mistral/actions/action_factory.py index dabf68a4c..c4a94dae3 100644 --- a/mistral/actions/action_factory.py +++ b/mistral/actions/action_factory.py @@ -15,6 +15,7 @@ # limitations under the License. import inspect +from oslo.config import cfg from mistral.actions import base from mistral.actions import std_actions @@ -26,36 +27,29 @@ from mistral.openstack.common import log as logging LOG = logging.getLogger(__name__) _ACTION_CTX_PARAM = 'action_context' -_STD_NAMESPACE = "std" - _NAMESPACES = {} -def _find_or_create_namespace(name): +def _find_or_create_namespace(full_name): + name = full_name.split('.')[-1] ns = _NAMESPACES.get(name) if not ns: - ns = base.Namespace(name) + ns = base.Namespace(full_name) _NAMESPACES[name] = ns return ns -def register_action_class(namespace_name, action_name, action_cls): - _find_or_create_namespace(namespace_name).add(action_name, action_cls) - - def get_registered_namespaces(): return _NAMESPACES.copy() -def _register_standard_action_classes(): - register_action_class(_STD_NAMESPACE, "echo", std_actions.EchoAction) - register_action_class(_STD_NAMESPACE, "http", std_actions.HTTPAction) - register_action_class(_STD_NAMESPACE, - "mistral_http", std_actions.MistralHTTPAction) - register_action_class(_STD_NAMESPACE, "ssh", std_actions.SSHAction) - register_action_class(_STD_NAMESPACE, "email", std_actions.SendEmailAction) +def _register_action_classes(): + cfg.CONF.import_opt('action_plugins', 'mistral.config') + for py_ns in cfg.CONF.action_plugins: + ns = _find_or_create_namespace(py_ns) + ns.log() def get_action_class(action_full_name): @@ -160,5 +154,5 @@ def create_action(db_task): (db_task, e)) -# Registering standard actions on module load. -_register_standard_action_classes() +# Registering actions on module load. +_register_action_classes() diff --git a/mistral/actions/base.py b/mistral/actions/base.py index f1acd3a52..ca9f79175 100644 --- a/mistral/actions/base.py +++ b/mistral/actions/base.py @@ -15,9 +15,9 @@ # limitations under the License. import abc +from stevedore import extension from mistral.openstack.common import log as logging -from mistral import exceptions as exc LOG = logging.getLogger(__name__) @@ -86,24 +86,26 @@ class Action(object): class Namespace(object): """Action namespace.""" - def __init__(self, name): - self.name = name - self.action_classes = {} # action name -> action class - - def add(self, action_name, action_cls): - if action_name in self.action_classes: - raise exc.ActionRegistrationException( - "Action is already associated with namespace " - "[action_cls=%s, namespace=%s" % - (action_cls, self)) - - self.action_classes[action_name] = action_cls + def __init__(self, namespace): + self.name = namespace.split('.')[-1] + self.mgr = extension.ExtensionManager( + namespace=namespace, + invoke_on_load=False) def contains_action_name(self, name): - return name in self.action_classes + return name in self.mgr.names() def get_action_class(self, name): - return self.action_classes.get(name) + # ExtensionManager has no "get" + if self.contains_action_name(name): + return self.mgr[name].plugin + else: + return None def __len__(self): - return len(self.action_classes) + # ExtensionManager has no len() + return len(self.mgr.names()) + + def log(self): + for ext in self.mgr: + LOG.debug('%s:%s' % (self.name, ext.name)) diff --git a/mistral/config.py b/mistral/config.py index 8265dfa17..19df90563 100644 --- a/mistral/config.py +++ b/mistral/config.py @@ -94,6 +94,11 @@ launch_opt = cfg.ListOpt( 'api, engine, and executor.' ) +action_plugins_opt = cfg.ListOpt( + 'action_plugins', + default=['mistral.actions.std'], + help='List of namespaces to search for plug-ins.') + wf_trace_log_name_opt = cfg.StrOpt('workflow_trace_log_name', default='workflow_trace', @@ -110,6 +115,7 @@ CONF.register_opts(db_opts, group='database') CONF.register_opts(rabbit_opts, group='rabbit') CONF.register_opts(executor_opts, group='executor') CONF.register_opt(wf_trace_log_name_opt) +CONF.register_opt(action_plugins_opt) CONF.register_cli_opt(use_debugger) CONF.register_cli_opt(launch_opt) diff --git a/requirements.txt b/requirements.txt index e84a90736..4e56e940d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ python-keystoneclient>=0.7.0 networkx>=1.8 six>=1.6.0 SQLAlchemy>=0.7.8,<=0.9.99 +stevedore>=0.14 yaql==0.2.1 # This is not in global requirements diff --git a/setup.cfg b/setup.cfg index 0c948fbdc..a3ae8b9b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,3 +30,10 @@ mistral.engine.drivers = mistral.executor.drivers = default = mistral.engine.drivers.default.executor:DefaultExecutor + +mistral.actions.std = + echo = mistral.actions.std_actions:EchoAction + http = mistral.actions.std_actions:HTTPAction + mistral_http = mistral.actions.std_actions:MistralHTTPAction + ssh = mistral.actions.std_actions:SSHAction + email = mistral.actions.std_actions:SendEmailAction