Passed
Push — develop ( a08f14...1c9535 )
by Plexxi
04:47 queued 02:22
created

ActionChainRunner._resolve_params()   B

Complexity

Conditions 2

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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