Completed
Pull Request — master (#2917)
by Lakshmi
10:03 queued 02:06
created

ActionChainRunner._resolve_params()   B

Complexity

Conditions 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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