File: base.py

package info (click to toggle)
python-mistral-lib 3.3.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 392 kB
  • sloc: python: 1,346; makefile: 21; sh: 2
file content (334 lines) | stat: -rw-r--r-- 12,622 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# 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