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
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
class InvalidActionException(MistralException):
|
|
||||||
def __init__(self, message=None):
|
|
||||||
super(InvalidActionException, self).__init__(message)
|
|
||||||
if message:
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class DBDuplicateEntry(MistralException):
|
class DBDuplicateEntry(MistralException):
|
||||||
message = "Database object already exists"
|
message = "Database object already exists"
|
||||||
code = "DB_DUPLICATE_ENTRY"
|
code = "DB_DUPLICATE_ENTRY"
|
||||||
@ -68,6 +61,23 @@ class ActionException(MistralException):
|
|||||||
self.message = message
|
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):
|
class EngineException(MistralException):
|
||||||
code = "ENGINE_ERROR"
|
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