BP mistral-actions-design (add new actions package)
* Base class for actions (Action) * Class for representing action namespaces (NameSpace) * Module action_factory.py for registering action classes, accessing them and creating action instances * Unit tests for action_factory.py * Standard actions (echo, http, email) TODO: * Ad-hoc per-workflow namespaces and actions * Mistral HTTP standard action * Refactoring executor to use new action infrastructure * Remove old action infrastructure * Action plugin architecture Partially implements: blueprint mistral-actions-design Partially implements: blueprint mistral-change-rest-api-to-http Change-Id: I8e79b1f8173809584f0156e666e1675f5bd7bb37
This commit is contained in:
parent
ebeb68b6dc
commit
32720f2c72
0
mistral/actions/__init__.py
Normal file
0
mistral/actions/__init__.py
Normal file
55
mistral/actions/action_factory.py
Normal file
55
mistral/actions/action_factory.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from mistral.actions import base
|
||||
from mistral.actions import std_actions
|
||||
|
||||
_STD_NAMESPACE = "std"
|
||||
|
||||
_NAMESPACES = {}
|
||||
|
||||
|
||||
def _find_or_create_namespace(name):
|
||||
ns = _NAMESPACES.get(name)
|
||||
|
||||
if not ns:
|
||||
ns = base.Namespace(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, "email", std_actions.SendEmailAction)
|
||||
|
||||
|
||||
def create_action(db_task):
|
||||
# TODO: Implement
|
||||
return None
|
||||
|
||||
|
||||
# Registering standard actions on module load.
|
||||
_register_standard_action_classes()
|
103
mistral/actions/base.py
Normal file
103
mistral/actions/base.py
Normal file
@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
|
||||
from mistral.openstack.common import log as logging
|
||||
from mistral import exceptions as exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action(object):
|
||||
"""Action.
|
||||
|
||||
Action is a means in Mistral to perform some useful work associated with
|
||||
a workflow during its execution. Every workflow task is configured with
|
||||
an action and when the task runs it eventually delegates to the action.
|
||||
When it happens task parameters get evaluated (calculating expressions,
|
||||
if any) and are treated as action parameters. So in a regular general
|
||||
purpose languages terminology action is a method declaration and task is
|
||||
a method call.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
self.namespace = namespace
|
||||
self.name = name
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self):
|
||||
"""Run action logic.
|
||||
|
||||
:return: result of the action. Note that for asynchronous actions
|
||||
it should always be None, however, if even it's not None it will be
|
||||
ignored by a caller.
|
||||
|
||||
In case if action failed this method must throw a ActionException
|
||||
to indicate that.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test(self):
|
||||
"""Returns action test result.
|
||||
|
||||
This method runs in test mode as a test version of method run() to
|
||||
generate and return a representative test result. It's basically a
|
||||
contract for action 'dry-run' behavior specifically useful for
|
||||
testing and workflow designing purposes.
|
||||
|
||||
:return: Representative action result.
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_sync(self):
|
||||
"""Returns True if the action is synchronous, otherwise False.
|
||||
|
||||
:return: True if the action is synchronous and method run() returns
|
||||
final action result. Otherwise returns False which means that
|
||||
a result of method run() should be ignored and a real action
|
||||
result is supposed to be delivered in an asynchronous manner
|
||||
using public API. By default, if a concrete implementation
|
||||
doesn't override this method then the action is synchronous.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
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 contains_action_name(self, name):
|
||||
return name in self.action_classes
|
||||
|
||||
def get_action_class(self, name):
|
||||
return self.action_classes.get(name)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.action_classes)
|
145
mistral/actions/std_actions.py
Normal file
145
mistral/actions/std_actions.py
Normal file
@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import requests
|
||||
|
||||
from mistral.openstack.common import log as logging
|
||||
from mistral.actions import base
|
||||
from mistral import exceptions as exc
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EchoAction(base.Action):
|
||||
"""Echo action.
|
||||
|
||||
This action just returns a configured value as a result without doing
|
||||
anything else. The value of such action implementation is that it
|
||||
can be used in development (for testing), demonstration and designing
|
||||
of workflows themselves where echo action can play the role of temporary
|
||||
stub.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name, output):
|
||||
super(EchoAction, self).__init__(namespace, name)
|
||||
self.output = output
|
||||
|
||||
def run(self):
|
||||
LOG.info('Running echo action [output=%s]' % self.output)
|
||||
|
||||
return self.output
|
||||
|
||||
def test(self):
|
||||
return 'Echo'
|
||||
|
||||
|
||||
class HTTPAction(base.Action):
|
||||
def __init__(self, namespace, name, url, params={},
|
||||
method="GET", headers={}, body={}):
|
||||
super(HTTPAction, self).__init__(namespace, name)
|
||||
|
||||
self.url = url
|
||||
self.params = params
|
||||
self.method = method
|
||||
self.headers = headers
|
||||
self.body = json.dumps(body) if isinstance(body, dict) else body
|
||||
|
||||
def run(self):
|
||||
LOG.info("Running HTTP action "
|
||||
"[url=%s, params=%s, method=%s, headers=%s, body=%s]" %
|
||||
(self.method,
|
||||
self.url,
|
||||
self.params,
|
||||
self.headers,
|
||||
self.body))
|
||||
|
||||
try:
|
||||
resp = requests.request(self.method,
|
||||
self.url,
|
||||
params=self.params,
|
||||
headers=self.headers,
|
||||
data=self.body)
|
||||
except Exception as e:
|
||||
raise exc.ActionException("Failed to send HTTP request: %s" % e)
|
||||
|
||||
LOG.info("HTTP action response:\n%s\n%s" %
|
||||
(resp.status_code, resp.content))
|
||||
|
||||
# TODO: Not sure we need to have this check here in base HTTP action.
|
||||
if resp.status_code not in range(200, 307):
|
||||
raise exc.ActionException("Received error HTTP code: %s" %
|
||||
resp.status_code)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
class SendEmailAction(base.Action):
|
||||
def __init__(self, namespace, name, params, settings):
|
||||
super(SendEmailAction, self).__init__(namespace, name)
|
||||
#TODO(dzimine): validate parameters
|
||||
|
||||
# Task invocation parameters.
|
||||
self.to = ', '.join(params['to'])
|
||||
self.subject = params['subject']
|
||||
self.body = params['body']
|
||||
|
||||
# Action provider settings.
|
||||
self.smtp_server = settings['smtp_server']
|
||||
self.sender = settings['from']
|
||||
self.password = settings['password'] \
|
||||
if 'password' in settings else None
|
||||
|
||||
def run(self):
|
||||
LOG.info("Sending email message "
|
||||
"[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]" %
|
||||
(self.sender, self.to, self.subject,
|
||||
self.smtp_server, self.body[:128]))
|
||||
|
||||
#TODO(dzimine): handle utf-8, http://stackoverflow.com/a/14506784
|
||||
message = MIMEText(self.body)
|
||||
message['Subject'] = self.subject
|
||||
message['From'] = self.sender
|
||||
message['To'] = self.to
|
||||
|
||||
try:
|
||||
s = smtplib.SMTP(self.smtp_server)
|
||||
|
||||
if self.password is not None:
|
||||
# Sequence to request TLS connection and log in (RFC-2487).
|
||||
s.ehlo()
|
||||
s.starttls()
|
||||
s.ehlo()
|
||||
s.login(self.sender, self.password)
|
||||
|
||||
s.sendmail(from_addr=self.sender,
|
||||
to_addrs=self.to,
|
||||
msg=message.as_string())
|
||||
except (smtplib.SMTPException, IOError) as e:
|
||||
raise exc.ActionException("Failed to send an email message: %s"
|
||||
% e)
|
||||
|
||||
def test(self):
|
||||
# Just logging the operation since this action is not supposed
|
||||
# to return a result.
|
||||
LOG.info("Sending email message "
|
||||
"[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]" %
|
||||
(self.sender, self.to, self.subject,
|
||||
self.smtp_server, self.body[:128]))
|
@ -42,13 +42,6 @@ class DataAccessException(MistralException):
|
||||
self.message = message
|
||||
|
||||
|
||||
class InvalidActionException(MistralException):
|
||||
def __init__(self, message=None):
|
||||
super(InvalidActionException, self).__init__(message)
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class DBDuplicateEntry(MistralException):
|
||||
message = "Database object already exists"
|
||||
code = "DB_DUPLICATE_ENTRY"
|
||||
@ -68,6 +61,23 @@ class ActionException(MistralException):
|
||||
self.message = message
|
||||
|
||||
|
||||
class InvalidActionException(MistralException):
|
||||
def __init__(self, message=None):
|
||||
super(InvalidActionException, self).__init__(message)
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class ActionRegistrationException(MistralException):
|
||||
message = "Failed to register action"
|
||||
code = "ACTION_REGISTRATION_ERROR"
|
||||
|
||||
def __init__(self, message=None):
|
||||
super(ActionRegistrationException, self).__init__(message)
|
||||
if message:
|
||||
self.message = message
|
||||
|
||||
|
||||
class EngineException(MistralException):
|
||||
code = "ENGINE_ERROR"
|
||||
|
||||
|
0
mistral/tests/unit/actions/__init__.py
Normal file
0
mistral/tests/unit/actions/__init__.py
Normal file
43
mistral/tests/unit/actions/test_action_factory.py
Normal file
43
mistral/tests/unit/actions/test_action_factory.py
Normal file
@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 - Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from mistral.openstack.common import log as logging
|
||||
from mistral.actions import action_factory
|
||||
from mistral.actions import std_actions as std
|
||||
from mistral.tests import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ActionFactoryTest(base.BaseTest):
|
||||
def test_register_standard_actions(self):
|
||||
namespaces = action_factory.get_registered_namespaces()
|
||||
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertIn("std", namespaces)
|
||||
|
||||
std_ns = namespaces["std"]
|
||||
|
||||
self.assertEqual(3, len(std_ns))
|
||||
|
||||
self.assertTrue(std_ns.contains_action_name("echo"))
|
||||
self.assertTrue(std_ns.contains_action_name("http"))
|
||||
self.assertTrue(std_ns.contains_action_name("email"))
|
||||
|
||||
self.assertEqual(std.EchoAction, std_ns.get_action_class("echo"))
|
||||
self.assertEqual(std.HTTPAction, std_ns.get_action_class("http"))
|
||||
self.assertEqual(std.SendEmailAction,
|
||||
std_ns.get_action_class("email"))
|
Loading…
Reference in New Issue
Block a user