GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — develop-v1.3.1 ( 8fa207...a1cf9b )
by
unknown
06:11
created

ActionChainRunner.run()   F

Complexity

Conditions 25

Size

Total Lines 171

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
dl 0
loc 171
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ActionChainRunner.run() 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.system import SYSTEM_KV_PREFIX
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_KV_PREFIX: KeyValueLookup()}
196
        context.update(action_parameters)
197
        return jinja_utils.render_values(mapping=vars, context=context)
198
199
    def get_node(self, node_name=None, raise_on_failure=False):
200
        if not node_name:
201
            return None
202
        for node in self.actionchain.chain:
203
            if node.name == node_name:
204
                return node
205
        if raise_on_failure:
206
            raise runnerexceptions.ActionRunnerException('Unable to find node with name "%s".' %
207
                                                         (node_name))
208
        return None
209
210
    def get_next_node(self, curr_node_name=None, condition='on-success'):
211
        if not curr_node_name:
212
            return self.get_node(self.actionchain.default)
213
        current_node = self.get_node(curr_node_name)
214
        if condition == 'on-success':
215
            return self.get_node(current_node.on_success, raise_on_failure=True)
216
        elif condition == 'on-failure':
217
            return self.get_node(current_node.on_failure, raise_on_failure=True)
218
        raise runnerexceptions.ActionRunnerException('Unknown condition %s.' % condition)
219
220
221
class ActionChainRunner(ActionRunner):
222
223
    def __init__(self, runner_id):
224
        super(ActionChainRunner, self).__init__(runner_id=runner_id)
225
        self.chain_holder = None
226
        self._meta_loader = MetaLoader()
227
        self._stopped = False
228
        self._skip_notify_tasks = []
229
        self._display_published = False
230
        self._chain_notify = None
231
232
    def pre_run(self):
233
        chainspec_file = self.entry_point
234
        LOG.debug('Reading action chain from %s for action %s.', chainspec_file,
235
                  self.action)
236
237
        try:
238
            chainspec = self._meta_loader.load(file_path=chainspec_file,
239
                                               expected_type=dict)
240
        except Exception as e:
241
            message = ('Failed to parse action chain definition from "%s": %s' %
242
                       (chainspec_file, str(e)))
243
            LOG.exception('Failed to load action chain definition.')
244
            raise runnerexceptions.ActionRunnerPreRunError(message)
245
246
        try:
247
            self.chain_holder = ChainHolder(chainspec, self.action_name)
248
        except json_schema_exceptions.ValidationError as e:
249
            # preserve the whole nasty jsonschema message as that is better to get to the
250
            # root cause
251
            message = str(e)
252
            LOG.exception('Failed to instantiate ActionChain.')
253
            raise runnerexceptions.ActionRunnerPreRunError(message)
254
        except Exception as e:
255
            message = e.message or str(e)
256
            LOG.exception('Failed to instantiate ActionChain.')
257
            raise runnerexceptions.ActionRunnerPreRunError(message)
258
259
        # Runner attributes are set lazily. So these steps
260
        # should happen outside the constructor.
261
        if getattr(self, 'liveaction', None):
262
            self._chain_notify = getattr(self.liveaction, 'notify', None)
263
        if self.runner_parameters:
264
            self._skip_notify_tasks = self.runner_parameters.get('skip_notify', [])
265
            self._display_published = self.runner_parameters.get('display_published', False)
266
267
        # Perform some pre-run chain validation
268
        try:
269
            self.chain_holder.validate()
270
        except Exception as e:
271
            raise runnerexceptions.ActionRunnerPreRunError(e.message)
272
273
    def run(self, action_parameters):
274
        # holds final result we store.
275
        result = {'tasks': []}
276
        # published variables are to be stored for display.
277
        if self._display_published:
278
            result[PUBLISHED_VARS_KEY] = {}
279
        context_result = {}  # holds result which is used for the template context purposes
280
        top_level_error = None  # stores a reference to a top level error
281
        fail = True
282
        action_node = None
283
284
        try:
285
            # initialize vars once we have the action_parameters. This allows
286
            # vars to refer to action_parameters.
287
            self.chain_holder.init_vars(action_parameters)
288
            action_node = self.chain_holder.get_next_node()
289
        except Exception as e:
290
            LOG.exception('Failed to get starting node "%s".', action_node.name)
291
292
            error = ('Failed to get starting node "%s". Lookup failed: %s' %
293
                     (action_node.name, str(e)))
294
            trace = traceback.format_exc(10)
295
            top_level_error = {
296
                'error': error,
297
                'traceback': trace
298
            }
299
300
        parent_context = {
301
            'execution_id': self.execution_id
302
        }
303
        if getattr(self.liveaction, 'context', None):
304
            parent_context.update(self.liveaction.context)
305
306
        while action_node:
307
            fail = False
308
            timeout = False
309
            error = None
310
            liveaction = None
311
312
            created_at = date_utils.get_datetime_utc_now()
313
314
            try:
315
                liveaction = self._get_next_action(
316
                    action_node=action_node, parent_context=parent_context,
317
                    action_params=action_parameters, context_result=context_result)
318
            except InvalidActionReferencedException as e:
319
                error = ('Failed to run task "%s". Action with reference "%s" doesn\'t exist.' %
320
                         (action_node.name, action_node.ref))
321
                LOG.exception(error)
322
323
                fail = True
324
                top_level_error = {
325
                    'error': error,
326
                    'traceback': traceback.format_exc(10)
327
                }
328
                break
329
            except ParameterRenderingFailedException as e:
330
                # Rendering parameters failed before we even got to running this action, abort and
331
                # fail the whole action chain
332
                LOG.exception('Failed to run action "%s".', action_node.name)
333
334
                fail = True
335
                error = ('Failed to run task "%s". Parameter rendering failed: %s' %
336
                         (action_node.name, str(e)))
337
                trace = traceback.format_exc(10)
338
                top_level_error = {
339
                    'error': error,
340
                    'traceback': trace
341
                }
342
                break
343
344
            try:
345
                liveaction = self._run_action(liveaction)
346
            except Exception as e:
347
                # Save the traceback and error message
348
                LOG.exception('Failure in running action "%s".', action_node.name)
349
350
                error = {
351
                    'error': 'Task "%s" failed: %s' % (action_node.name, str(e)),
352
                    'traceback': traceback.format_exc(10)
353
                }
354
                context_result[action_node.name] = error
355
            else:
356
                # Update context result
357
                context_result[action_node.name] = liveaction.result
358
359
                # Render and publish variables
360
                rendered_publish_vars = ActionChainRunner._render_publish_vars(
361
                    action_node=action_node, action_parameters=action_parameters,
362
                    execution_result=liveaction.result, previous_execution_results=context_result,
363
                    chain_vars=self.chain_holder.vars)
364
365
                if rendered_publish_vars:
366
                    self.chain_holder.vars.update(rendered_publish_vars)
367
                    if self._display_published:
368
                        result[PUBLISHED_VARS_KEY].update(rendered_publish_vars)
369
            finally:
370
                # Record result and resolve a next node based on the task success or failure
371
                updated_at = date_utils.get_datetime_utc_now()
372
373
                format_kwargs = {'action_node': action_node, 'liveaction_db': liveaction,
374
                                 'created_at': created_at, 'updated_at': updated_at}
375
376
                if error:
377
                    format_kwargs['error'] = error
378
379
                task_result = self._format_action_exec_result(**format_kwargs)
380
                result['tasks'].append(task_result)
381
382
                if self.liveaction_id:
383
                    self._stopped = action_service.is_action_canceled_or_canceling(
384
                        self.liveaction_id)
385
386
                if self._stopped:
387
                    LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
388
                    status = LIVEACTION_STATUS_CANCELED
389
                    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...
390
391
                try:
392
                    if not liveaction:
393
                        fail = True
394
                        action_node = self.chain_holder.get_next_node(action_node.name,
395
                                                                      condition='on-failure')
396
                    elif liveaction.status in LIVEACTION_FAILED_STATES:
397
                        if liveaction and liveaction.status == LIVEACTION_STATUS_TIMED_OUT:
398
                            timeout = True
399
                        else:
400
                            fail = True
401
                        action_node = self.chain_holder.get_next_node(action_node.name,
402
                                                                      condition='on-failure')
403
                    elif liveaction.status == LIVEACTION_STATUS_CANCELED:
404
                        # User canceled an action (task) in the workflow - cancel the execution of
405
                        # rest of the workflow
406
                        self._stopped = True
407
                        LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
408
                    elif liveaction.status == LIVEACTION_STATUS_SUCCEEDED:
409
                        action_node = self.chain_holder.get_next_node(action_node.name,
410
                                                                      condition='on-success')
411
                except Exception as e:
412
                    LOG.exception('Failed to get next node "%s".', action_node.name)
413
414
                    fail = True
415
                    error = ('Failed to get next node "%s". Lookup failed: %s' %
416
                             (action_node.name, str(e)))
417
                    trace = traceback.format_exc(10)
418
                    top_level_error = {
419
                        'error': error,
420
                        'traceback': trace
421
                    }
422
                    # reset action_node here so that chain breaks on failure.
423
                    action_node = None
424
                    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...
425
426
                if self._stopped:
427
                    LOG.info('Chain execution (%s) canceled by user.', self.liveaction_id)
428
                    status = LIVEACTION_STATUS_CANCELED
429
                    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...
430
431
        if fail:
432
            status = LIVEACTION_STATUS_FAILED
433
        elif timeout:
434
            status = LIVEACTION_STATUS_TIMED_OUT
435
        else:
436
            status = LIVEACTION_STATUS_SUCCEEDED
437
438
        if top_level_error:
439
            # Include top level error information
440
            result['error'] = top_level_error['error']
441
            result['traceback'] = top_level_error['traceback']
442
443
        return (status, result, None)
444
445
    @staticmethod
446
    def _render_publish_vars(action_node, action_parameters, execution_result,
447
                             previous_execution_results, chain_vars):
448
        """
449
        If no output is specified on the action_node the output is the entire execution_result.
450
        If any output is specified then only those variables are published as output of an
451
        execution of this action_node.
452
        The output variable can refer to a variable from the execution_result,
453
        previous_execution_results or chain_vars.
454
        """
455
        if not action_node.publish:
456
            return {}
457
458
        context = {}
459
        context.update(action_parameters)
460
        context.update({action_node.name: execution_result})
461
        context.update(previous_execution_results)
462
        context.update(chain_vars)
463
        context.update({RESULTS_KEY: previous_execution_results})
464
        context.update({SYSTEM_KV_PREFIX: KeyValueLookup()})
465
466
        try:
467
            rendered_result = jinja_utils.render_values(mapping=action_node.publish,
468
                                                        context=context)
469
        except Exception as e:
470
            key = getattr(e, 'key', None)
471
            value = getattr(e, 'value', None)
472
            msg = ('Failed rendering value for publish parameter "%s" in task "%s" '
473
                   '(template string=%s): %s' % (key, action_node.name, value, str(e)))
474
            raise ParameterRenderingFailedException(msg)
475
476
        return rendered_result
477
478
    @staticmethod
479
    def _resolve_params(action_node, original_parameters, results, chain_vars, chain_context):
480
        # setup context with original parameters and the intermediate results.
481
        context = {}
482
        context.update(original_parameters)
483
        context.update(results)
484
        context.update(chain_vars)
485
        context.update({RESULTS_KEY: results})
486
        context.update({SYSTEM_KV_PREFIX: KeyValueLookup()})
487
        context.update({ACTION_CONTEXT_KV_PREFIX: chain_context})
488
        try:
489
            rendered_params = jinja_utils.render_values(mapping=action_node.get_parameters(),
490
                                                        context=context)
491
        except Exception as e:
492
            LOG.exception('Jinja rendering for parameter "%s" failed.' % (e.key))
493
494
            key = getattr(e, 'key', None)
495
            value = getattr(e, 'value', None)
496
            msg = ('Failed rendering value for action parameter "%s" in task "%s" '
497
                   '(template string=%s): %s') % (key, action_node.name, value, str(e))
498
            raise ParameterRenderingFailedException(msg)
499
        LOG.debug('Rendered params: %s: Type: %s', rendered_params, type(rendered_params))
500
        return rendered_params
501
502
    def _get_next_action(self, action_node, parent_context, action_params, context_result):
503
        # Verify that the referenced action exists
504
        # TODO: We do another lookup in cast_param, refactor to reduce number of lookups
505
        task_name = action_node.name
506
        action_ref = action_node.ref
507
        action_db = action_db_util.get_action_by_ref(ref=action_ref)
508
509
        if not action_db:
510
            error = 'Task :: %s - Action with ref %s not registered.' % (task_name, action_ref)
511
            raise InvalidActionReferencedException(error)
512
513
        resolved_params = ActionChainRunner._resolve_params(
514
            action_node=action_node, original_parameters=action_params,
515
            results=context_result, chain_vars=self.chain_holder.vars,
516
            chain_context={'parent': parent_context})
517
518
        liveaction = self._build_liveaction_object(
519
            action_node=action_node,
520
            resolved_params=resolved_params,
521
            parent_context=parent_context)
522
523
        return liveaction
524
525
    def _run_action(self, liveaction, wait_for_completion=True, sleep_delay=1.0):
526
        """
527
        :param sleep_delay: Number of seconds to wait during "is completed" polls.
528
        :type sleep_delay: ``float``
529
        """
530
        try:
531
            # request return canceled
532
            liveaction, _ = action_service.request(liveaction)
533
        except Exception as e:
534
            liveaction.status = LIVEACTION_STATUS_FAILED
535
            LOG.exception('Failed to schedule liveaction.')
536
            raise e
537
538
        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...
539
            eventlet.sleep(sleep_delay)
540
            liveaction = action_db_util.get_liveaction_by_id(liveaction.id)
541
542
        return liveaction
543
544
    def _build_liveaction_object(self, action_node, resolved_params, parent_context):
545
        liveaction = LiveActionDB(action=action_node.ref)
546
547
        # Setup notify for task in chain.
548
        notify = self._get_notify(action_node)
549
        if notify:
550
            liveaction.notify = notify
551
            LOG.debug('%s: Task notify set to: %s', action_node.name, liveaction.notify)
552
553
        liveaction.context = {
554
            'parent': parent_context,
555
            'chain': vars(action_node)
556
        }
557
558
        liveaction.parameters = action_param_utils.cast_params(action_ref=action_node.ref,
559
                                                               params=resolved_params)
560
        return liveaction
561
562
    def _get_notify(self, action_node):
563
        if action_node.name not in self._skip_notify_tasks:
564
            if action_node.notify:
565
                task_notify = NotificationsHelper.to_model(action_node.notify)
566
                return task_notify
567
            elif self._chain_notify:
568
                return self._chain_notify
569
570
        return None
571
572
    def _format_action_exec_result(self, action_node, liveaction_db, created_at, updated_at,
573
                                   error=None):
574
        """
575
        Format ActionExecution result so it can be used in the final action result output.
576
577
        :rtype: ``dict``
578
        """
579
        assert isinstance(created_at, datetime.datetime)
580
        assert isinstance(updated_at, datetime.datetime)
581
582
        result = {}
583
584
        execution_db = None
585
        if liveaction_db:
586
            execution_db = ActionExecution.get(liveaction__id=str(liveaction_db.id))
587
588
        result['id'] = action_node.name
589
        result['name'] = action_node.name
590
        result['execution_id'] = str(execution_db.id) if execution_db else None
591
        result['workflow'] = None
592
593
        result['created_at'] = isotime.format(dt=created_at)
594
        result['updated_at'] = isotime.format(dt=updated_at)
595
596
        if error or not liveaction_db:
597
            result['state'] = LIVEACTION_STATUS_FAILED
598
        else:
599
            result['state'] = liveaction_db.status
600
601
        if error:
602
            result['result'] = error
603
        else:
604
            result['result'] = liveaction_db.result
605
606
        return result
607
608
609
def get_runner():
610
    return ActionChainRunner(str(uuid.uuid4()))
611