mistral-lib/mistral_lib/actions/base.py

335 lines
12 KiB
Python

# Copyright 2016 - Nokia Networks.
# Copyright 2017 - Red Hat, 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 oslo_utils import importutils
from mistral_lib import serialization
from mistral_lib.utils import inspect_utils as i_utils
class Action(serialization.MistralSerializable):
"""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.
Base action class initializer doesn't have arguments. However, concrete
action classes may have any number of parameters defining action behavior.
These parameters must correspond to parameters declared in action
specification (e.g. using DSL or others).
"""
def __init__(self):
# NOTE(d0ugal): We need to define an empty __init__ otherwise
# inspect.getargspec will fail in Python 2 for actions that subclass
# but don't define their own __init__.
pass
@abc.abstractmethod
def run(self, context):
"""Run action logic.
:param context: contains contextual information of the action.
The context includes an execution context (like execution identifier
and workflow name) and a security context with the authorization
details.
: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.
Result can be of two types:
1) Any serializable value meaningful from a user perspective (such
as string, number or dict).
2) Instance of {mistral_lib.types.Result} which has field "data"
for success result and field "error" for keeping so called "error
result" like HTTP error code and similar. Using the second type
allows to communicate a result even in case of error and hence to have
conditions in "on-error" clause of direct workflows. Depending on
particular action semantics one or another option may be preferable.
In case if action failed and there's no need to communicate any error
result this method should throw a ActionException.
"""
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
@classmethod
def get_serialization_key(cls):
# By default we use the same serializer key for every action
# assuming that most action class can use the same serializer.
return "%s.%s" % (Action.__module__, Action.__name__)
class ActionSerializer(serialization.DictBasedSerializer):
def serialize_to_dict(self, entity):
cls = type(entity)
return {
'cls': '%s.%s' % (cls.__module__, cls.__name__),
'cls_attrs': i_utils.get_public_fields(cls),
'data': vars(entity),
}
def deserialize_from_dict(self, entity_dict):
cls_str = entity_dict['cls']
# Rebuild action class and restore attributes.
cls = importutils.import_class(cls_str)
cls_attrs = entity_dict['cls_attrs']
if cls_attrs:
# If we have serialized class attributes it means that we need
# to create a dynamic class.
cls = type(cls.__name__, (cls,), cls_attrs)
# NOTE(rakhmerov): We use this hacky was of instantiating
# the action here because we can't use normal __init__(),
# we don't know the parameters. And even if we find out
# what they are the real internal state of the object is
# what was stored as vars() method that just returns all
# fields. So we have to bypass __init__() and set attributes
# one by one. Of course, this is a serious limitation since
# action creators will need to keep in mind to avoid having
# some complex initialisation logic in __init__() that
# does something not reflecting in an instance state.
# However, this all applies to the case when the action
# has to be sent to a remote executor.
action = cls.__new__(cls)
for k, v in entity_dict['data'].items():
setattr(action, k, v)
return action
# NOTE: Every action implementation can register its own serializer
# if needed, but this serializer should work for vast of majority of
# actions.
serialization.register_serializer(Action, ActionSerializer())
class ActionDescriptor(abc.ABC):
"""Provides required information about a certain type of actions.
Action descriptor is not an action itself. It rather carries all
important meta information about a particular action before the
action is instantiated. In some sense it is similar to a class but
the difference is that one type of action descriptor may support
many different actions. This abstraction is responsible for
validation of input parameters and instantiation of an action.
"""
@property
@abc.abstractmethod
def name(self):
"""The name of the action."""
pass
@property
@abc.abstractmethod
def description(self):
"""The description of the action."""
pass
@property
@abc.abstractmethod
def params_spec(self):
"""Comma-separated string with input parameter names.
Each parameter name can be either just a name or a string
"param=val" where "param" is the name of the parameter
and "val" its default value. The values are only indications
for the user and not used in the action instantiation process.
The string is split along the commas and then the parts along the
equal signs. Escaping is not possible.
"""
pass
@property
@abc.abstractmethod
def namespace(self):
"""The namespace of the action.
NOTE: Not all actions have to support namespaces.
"""
pass
@property
@abc.abstractmethod
def project_id(self):
"""The ID of the project (tenant) this action belongs to.
If it's not specified then the action can be used within
all projects (tenants).
NOTE: Not all actions have to support projects(tenants).
"""
pass
@property
@abc.abstractmethod
def scope(self):
"""The scope of the action within a project (tenant).
It makes sense only if the "project_id" property is not None.
It should be assigned with the "public" value if the action
is available in all projects and "private" if it's accessible
only by users of the specified project.
NOTE: Not all actions have to support projects(tenants).
"""
pass
@property
@abc.abstractmethod
def action_class_name(self):
"""String representation of the Python class of the action.
Can be None in case if the action is dynamically generated with
some kind of wrapper.
"""
pass
@property
@abc.abstractmethod
def action_class_attributes(self):
"""The attributes of the action Python class, if relevant.
If the action has a static Python class associated with it
and this method returns not an empty dictionary then the
action can be instantiated from a new dynamic class
based on the property "action_class_string" and this property
that essentially carries public class field values.
"""
pass
@abc.abstractmethod
def instantiate(self, input_dict, wf_ctx):
"""Instantiate the required action with the given parameters.
:param input_dict: Action parameters as a dictionary where keys
are parameter names and values are parameter values.
:param wf_ctx: Workflow context relevant for the point when
action is about to start.
:return: An instance of mistral_lib.actions.Action.
"""
pass
@abc.abstractmethod
def check_parameters(self, params):
"""Validate action parameters.
The method does a preliminary check of the given actual action
parameters and raises an exception if they don't match the
action signature. However, a successful invocation of this
method does not guarantee a further successful run of the
instantiated action.
:param params: Action parameters as a dictionary where keys
are parameter names and values are parameter values.
:return: None or raises an exception if the given parameters
are not valid.
"""
pass
@abc.abstractmethod
def post_process_result(self, result):
"""Converts the given action result.
A certain action implementation may need to do an additional
conversion of the action result by its descriptor. This approach
allows to implement wrapper actions running asynchronously
because in such cases, the initial action result depends on a 3rd
party that's responsible for delivering it to Mistral. But when
it comes to Mistral we still have a chance to apply needed
transformations defined by this method.
:param result: Action result.
An instance of mistral_lib.types.Result.
:return: Converted action result.
"""
pass
class ActionProvider(abc.ABC):
"""Serves as a source of actions for the system.
A concrete implementation of this interface can use its own
way of delivering actions to the system. It can store actions
in a database, get them over HTTP, AMQP or any other transport.
Or it can simply provide a static collection of actions and keep
them in memory throughout the cycle of the application.
"""
def __init__(self, name):
self._name = name
@property
def name(self):
"""The name of the action provider.
Different action providers can use it differently.
Some may completely ignore it, others may use it, for example,
for searching actions in a certain way.
"""
return self._name
@abc.abstractmethod
def find(self, action_name, namespace=None):
"""Finds action descriptor by name.
:param action_name: Action name.
:param namespace: Action namespace. None is used for the default
namespace.
:return: An instance of ActionDescriptor or None, if not found.
"""
pass
@abc.abstractmethod
def find_all(self, namespace=None, limit=None, sort_fields=None,
sort_dirs=None, filters=None):
"""Finds all action descriptors for this provider.
:param namespace: Optional. Action namespace.
:param limit: Positive integer or None. Maximum number of action
descriptors to return in a single result.
:param sort_fields: Optional. A list of action descriptor fields
that define sorting of the result set.
:param sort_dirs: Optional. A list of sorting orders ("asc" or "desc")
in addition to the "sort_fields" argument.
:param filters: Optional. A dictionary describing AND-joined filters.
:return: List of ActionDescriptor instances.
"""
pass