Completed
Pull Request — master (#2669)
by Lakshmi
06:56
created

ActionChainRunner   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 388
Duplicated Lines 0 %
Metric Value
dl 0
loc 388
rs 4.8387
wmc 58

10 Methods

Rating   Name   Duplication   Size   Complexity  
A _get_notify() 0 9 4
B _render_publish_vars() 0 33 3
D _format_action_exec_result() 0 35 8
B _resolve_params() 0 24 2
F run() 0 171 25
A _build_liveaction_object() 0 17 2
A _get_next_action() 0 22 2
A _run_action() 0 18 4
C pre_run() 0 40 7
A __init__() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like ActionChainRunner often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import eventlet
17
import traceback
18
import uuid
19
import datetime
20
21
from jsonschema import exceptions as json_schema_exceptions
22
23
from st2actions.runners import ActionRunner
24
from st2common import log as logging
25
from st2common.constants.action import ACTION_CONTEXT_KV_PREFIX
26
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED
27
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT
28
from st2common.constants.action import LIVEACTION_STATUS_FAILED
29
from st2common.constants.action import LIVEACTION_STATUS_CANCELED
30
from st2common.constants.action import LIVEACTION_COMPLETED_STATES
31
from st2common.constants.action import LIVEACTION_FAILED_STATES
32
from st2common.constants.keyvalue import SYSTEM_SCOPE, USER_SCOPE
33
from st2common.content.loader import MetaLoader
34
from st2common.exceptions.action import (ParameterRenderingFailedException,
35
                                         InvalidActionReferencedException)
36
from st2common.exceptions import actionrunner as runnerexceptions
37
from st2common.models.api.notification import NotificationsHelper
38
from st2common.models.db.liveaction import LiveActionDB
39
from st2common.models.system import actionchain
40
from st2common.models.utils import action_param_utils
41
from st2common.persistence.execution import ActionExecution
42
from st2common.services import action as action_service
43
from st2common.services.keyvalues import KeyValueLookup
44
from st2common.util import action_db as action_db_util
45
from st2common.util import isotime
46
from st2common.util import date as date_utils
47
from st2common.util import jinja as jinja_utils
48
49
50
LOG = logging.getLogger(__name__)
51
RESULTS_KEY = '__results'
52
JINJA_START_MARKERS = [
53
    '{{',
54
    '{%'
55
]
56
PUBLISHED_VARS_KEY = 'published'
57
58
59
class ChainHolder(object):
60
61
    def __init__(self, chainspec, chainname):
62
        self.actionchain = actionchain.ActionChain(**chainspec)
63
        self.chainname = chainname
64
65
        if not self.actionchain.default:
66
            default = self._get_default(self.actionchain)
67
            self.actionchain.default = default
68
69
        LOG.debug('Using %s as default for %s.', self.actionchain.default, self.chainname)
70
        if not self.actionchain.default:
71
            raise Exception('Failed to find default node in %s.' % (self.chainname))
72
73
        self.vars = {}
74
75
    def init_vars(self, action_parameters):
76
        if self.actionchain.vars:
77
            self.vars = self._get_rendered_vars(self.actionchain.vars,
78
                                                action_parameters=action_parameters)
79
80
    def validate(self):
81
        """
82
        Function which performs a simple compile time validation.
83
84
        Keep in mind that some variables are only resolved during run time which means we can
85
        perform only simple validation during compile / create time.
86
        """
87
        all_nodes = self._get_all_nodes(action_chain=self.actionchain)
88
89
        for node in self.actionchain.chain:
90
            on_success_node_name = node.on_success
91
            on_failure_node_name = node.on_failure
92
93
            # Check "on-success" path
94
            valid_name = self._is_valid_node_name(all_node_names=all_nodes,
95
                                                  node_name=on_success_node_name)
96
            if not valid_name:
97
                msg = ('Unable to find node with name "%s" referenced in "on-success" in '
98
                       'task "%s".' % (on_success_node_name, node.name))
99
                raise ValueError(msg)
100
101
            # Check "on-failure" path
102
            valid_name = self._is_valid_node_name(all_node_names=all_nodes,
103
                                                  node_name=on_failure_node_name)
104
            if not valid_name:
105
                msg = ('Unable to find node with name "%s" referenced in "on-failure" in '
106
                       'task "%s".' % (on_failure_node_name, node.name))
107
                raise ValueError(msg)
108
109
        # check if node specified in default is valid.
110
        if self.actionchain.default:
111
            valid_name = self._is_valid_node_name(all_node_names=all_nodes,
112
                                                  node_name=self.actionchain.default)
113
            if not valid_name:
114
                msg = ('Unable to find node with name "%s" referenced in "default".' %
115
                       self.actionchain.default)
116
                raise ValueError(msg)
117
        return True
118
119
    @staticmethod
120
    def _get_default(action_chain):
121
        # default is defined
122
        if action_chain.default:
123
            return action_chain.default
124
        # no nodes in chain
125
        if not action_chain.chain:
126
            return None
127
        # The first node with no references is the default node. Assumptions
128
        # that support this are :
129
        # 1. There are no loops in the chain. Even if there are loops there is
130
        #    at least 1 node which does not end up in this loop.
131
        # 2. There are no fragments in the chain.
132
        all_nodes = ChainHolder._get_all_nodes(action_chain=action_chain)
133
        node_names = set(all_nodes)
134
        on_success_nodes = ChainHolder._get_all_on_success_nodes(action_chain=action_chain)
135
        on_failure_nodes = ChainHolder._get_all_on_failure_nodes(action_chain=action_chain)
136
        referenced_nodes = on_success_nodes | on_failure_nodes
137
        possible_default_nodes = node_names - referenced_nodes
138
        if possible_default_nodes:
139
            # This is to preserve order. set([..]) does not preserve the order so iterate
140
            # over original array.
141
            for node in all_nodes:
142
                if node in possible_default_nodes:
143
                    return node
144
        # If no node is found assume the first node in the chain list to be default.
145
        return action_chain.chain[0].name
146
147
    @staticmethod
148
    def _get_all_nodes(action_chain):
149
        """
150
        Return names for all the nodes in the chain.
151
        """
152
        all_nodes = [node.name for node in action_chain.chain]
153
        return all_nodes
154
155
    @staticmethod
156
    def _get_all_on_success_nodes(action_chain):
157
        """
158
        Return names for all the tasks referenced in "on-success".
159
        """
160
        on_success_nodes = set([node.on_success for node in action_chain.chain])
161
        return on_success_nodes
162
163
    @staticmethod
164
    def _get_all_on_failure_nodes(action_chain):
165
        """
166
        Return names for all the tasks referenced in "on-failure".
167
        """
168
        on_failure_nodes = set([node.on_failure for node in action_chain.chain])
169
        return on_failure_nodes
170
171
    def _is_valid_node_name(self, all_node_names, node_name):
172
        """
173
        Function which validates that the provided node name is defined in the workflow definition
174
        and it's valid.
175
176
        Keep in mind that we can only perform validation for task names which don't include jinja
177
        expressions since those are rendered at run time.
178
        """
179
        if not node_name:
180
            # This task name needs to be resolved during run time so we cant validate the name now
181
            return True
182
183
        for jinja_start_marker in JINJA_START_MARKERS:
184
            if jinja_start_marker in node_name:
185
                # This task name needs to be resolved during run time so we cant validate the name
186
                # now
187
                return True
188
189
        return node_name in all_node_names
190
191
    @staticmethod
192
    def _get_rendered_vars(vars, action_parameters):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in vars.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
193
        if not vars:
194
            return {}
195
        context = {SYSTEM_SCOPE: KeyValueLookup(scope=SYSTEM_SCOPE)}
196
        context[USER_SCOPE] = KeyValueLookup(scope=USER_SCOPE)
197
        context.update(action_parameters)
198
        return jinja_utils.render_values(mapping=vars, context=context)
199
200
    def get_node(self, node_name=None, raise_on_failure=False):
201
        if not node_name:
202
            return None
203
        for node in self.actionchain.chain:
204
            if node.name == node_name:
205
                return node
206
        if raise_on_failure:
207
            raise runnerexceptions.ActionRunnerException('Unable to find node with name "%s".' %
208
                                                         (node_name))
209
        return None
210
211
    def get_next_node(self, curr_node_name=None, condition='on-success'):
212
        if not curr_node_name:
213
            return self.get_node(self.actionchain.default)
214
        current_node = self.get_node(curr_node_name)
215
        if condition == 'on-success':
216
            return self.get_node(current_node.on_success, raise_on_failure=True)
217
        elif condition == 'on-failure':
218
            return self.get_node(current_node.on_failure, raise_on_failure=True)
219
        raise runnerexceptions.ActionRunnerException('Unknown condition %s.' % condition)
220
221
222
class ActionChainRunner(ActionRunner):
223
224
    def __init__(self, runner_id):
225
        super(ActionChainRunner, self).__init__(runner_id=runner_id)
226
        self.chain_holder = None
227
        self._meta_loader = MetaLoader()
228
        self._stopped = False
229
        self._skip_notify_tasks = []
230
        self._display_published = False
231
        self._chain_notify = None
232
233
    def pre_run(self):
234
        chainspec_file = self.entry_point
235
        LOG.debug('Reading action chain from %s for action %s.', chainspec_file,
236
                  self.action)
237
238
        try:
239
            chainspec = self._meta_loader.load(file_path=chainspec_file,
240
                                               expected_type=dict)
241
        except Exception as e:
242
            message = ('Failed to parse action chain definition from "%s": %s' %
243
                       (chainspec_file, str(e)))
244
            LOG.exception('Failed to load action chain definition.')
245
            raise runnerexceptions.ActionRunnerPreRunError(message)
246
247
        try:
248
            self.chain_holder = ChainHolder(chainspec, self.action_name)
249
        except json_schema_exceptions.ValidationError as e:
250
            # preserve the whole nasty jsonschema message as that is better to get to the
251
            # root cause
252
            message = str(e)
253
            LOG.exception('Failed to instantiate ActionChain.')
254
            raise runnerexceptions.ActionRunnerPreRunError(message)
255
        except Exception as e:
256
            message = e.message or str(e)
257
            LOG.exception('Failed to instantiate ActionChain.')
258
            raise runnerexceptions.ActionRunnerPreRunError(message)
259
260
        # Runner attributes are set lazily. So these steps
261
        # should happen outside the constructor.
262
        if getattr(self, 'liveaction', None):
263
            self._chain_notify = getattr(self.liveaction, 'notify', None)
264
        if self.runner_parameters:
265
            self._skip_notify_tasks = self.runner_parameters.get('skip_notify', [])
266
            self._display_published = self.runner_parameters.get('display_published', False)
267
268
        # Perform some pre-run chain validation
269
        try:
270
            self.chain_holder.validate()
271
        except Exception as e:
272
            raise runnerexceptions.ActionRunnerPreRunError(e.message)
273
274
    def run(self, action_parameters):
275
        # holds final result we store.
276
        result = {'tasks': []}
277
        # published variables are to be stored for display.
278
        if self._display_published:
279
            result[PUBLISHED_VARS_KEY] = {}
280
        context_result = {}  # holds result which is used for the template context purposes
281
        top_level_error = None  # stores a reference to a top level error
282
        fail = True
283
        action_node = None
284
285
        try:
286
            # initialize vars once we have the action_parameters. This allows
287
            # vars to refer to action_parameters.
288
            self.chain_holder.init_vars(action_parameters)
289
            action_node = self.chain_holder.get_next_node()
290
        except Exception as e:
291
            LOG.exception('Failed to get starting node "%s".', action_node.name)
292
293
            error = ('Failed to get starting node "%s". Lookup failed: %s' %
294
                     (action_node.name, str(e)))
295
            trace = traceback.format_exc(10)
296
            top_level_error = {
297
                'error': error,
298
                'traceback': trace
299
            }
300
301
        parent_context = {
302
            'execution_id': self.execution_id
303
        }
304
        if getattr(self.liveaction, 'context', None):
305
            parent_context.update(self.liveaction.context)
306
307
        while action_node:
308
            fail = False
309
            timeout = False
310
            error = None
311
            liveaction = None
312
313
            created_at = date_utils.get_datetime_utc_now()
314
315
            try:
316
                liveaction = self._get_next_action(
317
                    action_node=action_node, parent_context=parent_context,
318
                    action_params=action_parameters, context_result=context_result)
319
            except InvalidActionReferencedException as e:
320
                error = ('Failed to run task "%s". Action with reference "%s" doesn\'t exist.' %
321
                         (action_node.name, action_node.ref))
322
                LOG.exception(error)
323
324
                fail = True
325
                top_level_error = {
326
                    'error': error,
327
                    'traceback': traceback.format_exc(10)
328
                }
329
                break
330
            except ParameterRenderingFailedException as e:
331
                # Rendering parameters failed before we even got to running this action, abort and
332
                # fail the whole action chain
333
                LOG.exception('Failed to run action "%s".', action_node.name)
334
335
                fail = True
336
                error = ('Failed to run task "%s". Parameter rendering failed: %s' %
337
                         (action_node.name, str(e)))
338
                trace = traceback.format_exc(10)
339
                top_level_error = {
340
                    'error': error,
341
                    'traceback': trace
342
                }
343
                break
344
345
            try:
346
                liveaction = self._run_action(liveaction)
347
            except Exception as e:
348
                # Save the traceback and error message
349
                LOG.exception('Failure in running action "%s".', action_node.name)
350
351
                error = {
352
                    'error': 'Task "%s" failed: %s' % (action_node.name, str(e)),
353
                    'traceback': traceback.format_exc(10)
354
                }
355
                context_result[action_node.name] = error
356
            else:
357
                # Update context result
358
                context_result[action_node.name] = liveaction.result
359
360
                # Render and publish variables
361
                rendered_publish_vars = ActionChainRunner._render_publish_vars(
362
                    action_node=action_node, action_parameters=action_parameters,
363
                    execution_result=liveaction.result, previous_execution_results=context_result,
364
                    chain_vars=self.chain_holder.vars)
365
366
                if rendered_publish_vars:
367
                    self.chain_holder.vars.update(rendered_publish_vars)
368
                    if self._display_published:
369
                        result[PUBLISHED_VARS_KEY].update(rendered_publish_vars)
370
            finally:
371
                # Record result and resolve a next node based on the task success or failure
372
                updated_at = date_utils.get_datetime_utc_now()
373
374
                format_kwargs = {'action_node': action_node, 'liveaction_db': liveaction,
375
                                 'created_at': created_at, 'updated_at': updated_at}
376
377
                if error:
378
                    format_kwargs['error'] = error
379
380
                task_result = self._format_action_exec_result(**format_kwargs)
381
                result['tasks'].append(task_result)
382
383
                if self.liveaction_id:
384
                    self._stopped = action_service.is_action_canceled_or_canceling(
385
                        self.liveaction_id)
386
387
                if self._stopped:
388
                    LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
389
                    status = LIVEACTION_STATUS_CANCELED
390
                    return (status, result, None)
0 ignored issues
show
Bug Best Practice introduced by
return statements in finally blocks should be avoided.

Placing a return statement inside finally will swallow all exceptions that may have been thrown in the try block.

Loading history...
391
392
                try:
393
                    if not liveaction:
394
                        fail = True
395
                        action_node = self.chain_holder.get_next_node(action_node.name,
396
                                                                      condition='on-failure')
397
                    elif liveaction.status in LIVEACTION_FAILED_STATES:
398
                        if liveaction and liveaction.status == LIVEACTION_STATUS_TIMED_OUT:
399
                            timeout = True
400
                        else:
401
                            fail = True
402
                        action_node = self.chain_holder.get_next_node(action_node.name,
403
                                                                      condition='on-failure')
404
                    elif liveaction.status == LIVEACTION_STATUS_CANCELED:
405
                        # User canceled an action (task) in the workflow - cancel the execution of
406
                        # rest of the workflow
407
                        self._stopped = True
408
                        LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
409
                    elif liveaction.status == LIVEACTION_STATUS_SUCCEEDED:
410
                        action_node = self.chain_holder.get_next_node(action_node.name,
411
                                                                      condition='on-success')
412
                except Exception as e:
413
                    LOG.exception('Failed to get next node "%s".', action_node.name)
414
415
                    fail = True
416
                    error = ('Failed to get next node "%s". Lookup failed: %s' %
417
                             (action_node.name, str(e)))
418
                    trace = traceback.format_exc(10)
419
                    top_level_error = {
420
                        'error': error,
421
                        'traceback': trace
422
                    }
423
                    # reset action_node here so that chain breaks on failure.
424
                    action_node = None
425
                    break
0 ignored issues
show
Bug Best Practice introduced by
break statement in finally block may swallow exception

Placing a return statement inside finally will swallow all exceptions that may have been thrown in the try block.

Loading history...
426
427
                if self._stopped:
428
                    LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
429
                    status = LIVEACTION_STATUS_CANCELED
430
                    return (status, result, None)
0 ignored issues
show
Bug Best Practice introduced by
return statements in finally blocks should be avoided.

Placing a return statement inside finally will swallow all exceptions that may have been thrown in the try block.

Loading history...
431
432
        if fail:
433
            status = LIVEACTION_STATUS_FAILED
434
        elif timeout:
435
            status = LIVEACTION_STATUS_TIMED_OUT
436
        else:
437
            status = LIVEACTION_STATUS_SUCCEEDED
438
439
        if top_level_error:
440
            # Include top level error information
441
            result['error'] = top_level_error['error']
442
            result['traceback'] = top_level_error['traceback']
443
444
        return (status, result, None)
445
446
    @staticmethod
447
    def _render_publish_vars(action_node, action_parameters, execution_result,
448
                             previous_execution_results, chain_vars):
449
        """
450
        If no output is specified on the action_node the output is the entire execution_result.
451
        If any output is specified then only those variables are published as output of an
452
        execution of this action_node.
453
        The output variable can refer to a variable from the execution_result,
454
        previous_execution_results or chain_vars.
455
        """
456
        if not action_node.publish:
457
            return {}
458
459
        context = {}
460
        context.update(action_parameters)
461
        context.update({action_node.name: execution_result})
462
        context.update(previous_execution_results)
463
        context.update(chain_vars)
464
        context.update({RESULTS_KEY: previous_execution_results})
465
        context.update({SYSTEM_SCOPE: KeyValueLookup(scope=SYSTEM_SCOPE)})
466
        context.update({USER_SCOPE: KeyValueLookup(scope=USER_SCOPE)})
467
468
        try:
469
            rendered_result = jinja_utils.render_values(mapping=action_node.publish,
470
                                                        context=context)
471
        except Exception as e:
472
            key = getattr(e, 'key', None)
473
            value = getattr(e, 'value', None)
474
            msg = ('Failed rendering value for publish parameter "%s" in task "%s" '
475
                   '(template string=%s): %s' % (key, action_node.name, value, str(e)))
476
            raise ParameterRenderingFailedException(msg)
477
478
        return rendered_result
479
480
    @staticmethod
481
    def _resolve_params(action_node, original_parameters, results, chain_vars, chain_context):
482
        # setup context with original parameters and the intermediate results.
483
        context = {}
484
        context.update(original_parameters)
485
        context.update(results)
486
        context.update(chain_vars)
487
        context.update({RESULTS_KEY: results})
488
        context.update({SYSTEM_SCOPE: KeyValueLookup(scope=SYSTEM_SCOPE)})
489
        context.update({USER_SCOPE: KeyValueLookup(scope=USER_SCOPE)})
490
        context.update({ACTION_CONTEXT_KV_PREFIX: chain_context})
491
        try:
492
            rendered_params = jinja_utils.render_values(mapping=action_node.get_parameters(),
493
                                                        context=context)
494
        except Exception as e:
495
            LOG.exception('Jinja rendering for parameter "%s" failed.' % (e.key))
496
497
            key = getattr(e, 'key', None)
498
            value = getattr(e, 'value', None)
499
            msg = ('Failed rendering value for action parameter "%s" in task "%s" '
500
                   '(template string=%s): %s') % (key, action_node.name, value, str(e))
501
            raise ParameterRenderingFailedException(msg)
502
        LOG.debug('Rendered params: %s: Type: %s', rendered_params, type(rendered_params))
503
        return rendered_params
504
505
    def _get_next_action(self, action_node, parent_context, action_params, context_result):
506
        # Verify that the referenced action exists
507
        # TODO: We do another lookup in cast_param, refactor to reduce number of lookups
508
        task_name = action_node.name
509
        action_ref = action_node.ref
510
        action_db = action_db_util.get_action_by_ref(ref=action_ref)
511
512
        if not action_db:
513
            error = 'Task :: %s - Action with ref %s not registered.' % (task_name, action_ref)
514
            raise InvalidActionReferencedException(error)
515
516
        resolved_params = ActionChainRunner._resolve_params(
517
            action_node=action_node, original_parameters=action_params,
518
            results=context_result, chain_vars=self.chain_holder.vars,
519
            chain_context={'parent': parent_context})
520
521
        liveaction = self._build_liveaction_object(
522
            action_node=action_node,
523
            resolved_params=resolved_params,
524
            parent_context=parent_context)
525
526
        return liveaction
527
528
    def _run_action(self, liveaction, wait_for_completion=True, sleep_delay=1.0):
529
        """
530
        :param sleep_delay: Number of seconds to wait during "is completed" polls.
531
        :type sleep_delay: ``float``
532
        """
533
        try:
534
            # request return canceled
535
            liveaction, _ = action_service.request(liveaction)
536
        except Exception as e:
537
            liveaction.status = LIVEACTION_STATUS_FAILED
538
            LOG.exception('Failed to schedule liveaction.')
539
            raise e
540
541
        while (wait_for_completion and liveaction.status not in LIVEACTION_COMPLETED_STATES):
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after while.
Loading history...
542
            eventlet.sleep(sleep_delay)
543
            liveaction = action_db_util.get_liveaction_by_id(liveaction.id)
544
545
        return liveaction
546
547
    def _build_liveaction_object(self, action_node, resolved_params, parent_context):
548
        liveaction = LiveActionDB(action=action_node.ref)
549
550
        # Setup notify for task in chain.
551
        notify = self._get_notify(action_node)
552
        if notify:
553
            liveaction.notify = notify
554
            LOG.debug('%s: Task notify set to: %s', action_node.name, liveaction.notify)
555
556
        liveaction.context = {
557
            'parent': parent_context,
558
            'chain': vars(action_node)
559
        }
560
561
        liveaction.parameters = action_param_utils.cast_params(action_ref=action_node.ref,
562
                                                               params=resolved_params)
563
        return liveaction
564
565
    def _get_notify(self, action_node):
566
        if action_node.name not in self._skip_notify_tasks:
567
            if action_node.notify:
568
                task_notify = NotificationsHelper.to_model(action_node.notify)
569
                return task_notify
570
            elif self._chain_notify:
571
                return self._chain_notify
572
573
        return None
574
575
    def _format_action_exec_result(self, action_node, liveaction_db, created_at, updated_at,
576
                                   error=None):
577
        """
578
        Format ActionExecution result so it can be used in the final action result output.
579
580
        :rtype: ``dict``
581
        """
582
        assert isinstance(created_at, datetime.datetime)
583
        assert isinstance(updated_at, datetime.datetime)
584
585
        result = {}
586
587
        execution_db = None
588
        if liveaction_db:
589
            execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id))
590
591
        result['id'] = action_node.name
592
        result['name'] = action_node.name
593
        result['execution_id'] = str(execution_db.id) if execution_db else None
594
        result['workflow'] = None
595
596
        result['created_at'] = isotime.format(dt=created_at)
597
        result['updated_at'] = isotime.format(dt=updated_at)
598
599
        if error or not liveaction_db:
600
            result['state'] = LIVEACTION_STATUS_FAILED
601
        else:
602
            result['state'] = liveaction_db.status
603
604
        if error:
605
            result['result'] = error
606
        else:
607
            result['result'] = liveaction_db.result
608
609
        return result
610
611
612
def get_runner():
613
    return ActionChainRunner(str(uuid.uuid4()))
614