Test Failed
Pull Request — master (#3496)
by Lakshmi
05:35
created

ActionRunCommandMixin   F

Complexity

Total Complexity 124

Size/Duplication

Total Lines 609
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 609
rs 1.5505
c 2
b 0
f 0
wmc 124

23 Methods

Rating   Name   Duplication   Size   Complexity  
F _print_execution_details() 0 39 10
A get_resource() 0 2 1
B _add_common_options() 0 33 1
A run_and_print() 0 15 4
A transform_object() 0 18 4
B _format_for_common_representation() 0 20 5
B _get_execution_result() 0 22 6
A read_file() 0 11 4
A _get_inherited_env_vars() 0 10 4
A _get_task_result() 0 5 2
B normalize() 0 19 7
F _format_child_instances() 0 33 9
B _get_task_error() 0 22 4
A _sort_parameters() 0 13 2
B transform_array() 0 28 4
D _run_and_print_child_task_list() 0 73 10
A _get_parameter_sort_value() 0 15 2
A _get_top_level_error() 0 14 2
C _get_params_types() 0 25 7
C _print_param() 0 20 10
F _get_action_parameters_from_args() 0 163 33
F _print_help() 0 50 12
A is_immutable() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like ActionRunCommandMixin 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 os
17
import ast
18
import copy
19
import json
20
import logging
21
import textwrap
22
import calendar
23
import time
24
import six
25
import sys
26
27
from os.path import join as pjoin
28
29
from st2client import models
30
from st2client.commands import resource
31
from st2client.commands.resource import add_auth_token_to_kwargs_from_cli
32
from st2client.exceptions.operations import OperationFailureException
33
from st2client.formatters import table
34
from st2client.formatters import execution as execution_formatter
35
from st2client.utils import jsutil
36
from st2client.utils.date import format_isodate_for_user_timezone
37
from st2client.utils.date import parse as parse_isotime
38
from st2client.utils.color import format_status
39
40
LOG = logging.getLogger(__name__)
41
42
LIVEACTION_STATUS_REQUESTED = 'requested'
43
LIVEACTION_STATUS_SCHEDULED = 'scheduled'
44
LIVEACTION_STATUS_DELAYED = 'delayed'
45
LIVEACTION_STATUS_RUNNING = 'running'
46
LIVEACTION_STATUS_SUCCEEDED = 'succeeded'
47
LIVEACTION_STATUS_FAILED = 'failed'
48
LIVEACTION_STATUS_TIMED_OUT = 'timeout'
49
LIVEACTION_STATUS_ABANDONED = 'abandoned'
50
LIVEACTION_STATUS_CANCELING = 'canceling'
51
LIVEACTION_STATUS_CANCELED = 'canceled'
52
53
54
LIVEACTION_COMPLETED_STATES = [
55
    LIVEACTION_STATUS_SUCCEEDED,
56
    LIVEACTION_STATUS_FAILED,
57
    LIVEACTION_STATUS_TIMED_OUT,
58
    LIVEACTION_STATUS_CANCELED,
59
    LIVEACTION_STATUS_ABANDONED
60
]
61
62
# Who parameters should be masked when displaying action execution output
63
PARAMETERS_TO_MASK = [
64
    'password',
65
    'private_key'
66
]
67
68
# A list of environment variables which are never inherited when using run
69
# --inherit-env flag
70
ENV_VARS_BLACKLIST = [
71
    'pwd',
72
    'mail',
73
    'username',
74
    'user',
75
    'path',
76
    'home',
77
    'ps1',
78
    'shell',
79
    'pythonpath',
80
    'ssh_tty',
81
    'ssh_connection',
82
    'lang',
83
    'ls_colors',
84
    'logname',
85
    'oldpwd',
86
    'term',
87
    'xdg_session_id'
88
]
89
90
WORKFLOW_RUNNER_TYPES = [
91
    'action-chain',
92
    'mistral-v2',
93
]
94
95
96
def format_parameters(value):
97
    # Mask sensitive parameters
98
    if not isinstance(value, dict):
99
        # No parameters, leave it as it is
100
        return value
101
102
    for param_name, _ in value.items():
103
        if param_name in PARAMETERS_TO_MASK:
104
            value[param_name] = '********'
105
106
    return value
107
108
109
# String for indenting etc.
110
WF_PREFIX = '+ '
111
NON_WF_PREFIX = '  '
112
INDENT_CHAR = ' '
113
114
115
def format_wf_instances(instances):
116
    """
117
    Adds identification characters to a workflow and appropriately shifts
118
    the non-workflow instances. If no workflows are found does nothing.
119
    """
120
    # only add extr chars if there are workflows.
121
    has_wf = False
122
    for instance in instances:
123
        if not getattr(instance, 'children', None):
124
            continue
125
        else:
126
            has_wf = True
127
            break
128
    if not has_wf:
129
        return instances
130
    # Prepend wf and non_wf prefixes.
131
    for instance in instances:
132
        if getattr(instance, 'children', None):
133
            instance.id = WF_PREFIX + instance.id
134
        else:
135
            instance.id = NON_WF_PREFIX + instance.id
136
    return instances
137
138
139
def format_execution_statuses(instances):
140
    result = []
141
    for instance in instances:
142
        instance = format_execution_status(instance)
143
        result.append(instance)
144
145
    return result
146
147
148
def format_execution_status(instance):
149
    """
150
    Augment instance "status" attribute with number of seconds which have elapsed for all the
151
    executions which are in running state and execution total run time for all the executions
152
    which have finished.
153
    """
154
    start_timestamp = getattr(instance, 'start_timestamp', None)
155
    end_timestamp = getattr(instance, 'end_timestamp', None)
156
157
    if instance.status == LIVEACTION_STATUS_RUNNING and start_timestamp:
158
        start_timestamp = instance.start_timestamp
159
        start_timestamp = parse_isotime(start_timestamp)
160
        start_timestamp = calendar.timegm(start_timestamp.timetuple())
161
        now = int(time.time())
162
        elapsed_seconds = (now - start_timestamp)
163
        instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds)
164
    elif instance.status in LIVEACTION_COMPLETED_STATES and start_timestamp and end_timestamp:
165
        start_timestamp = parse_isotime(start_timestamp)
166
        start_timestamp = calendar.timegm(start_timestamp.timetuple())
167
        end_timestamp = parse_isotime(end_timestamp)
168
        end_timestamp = calendar.timegm(end_timestamp.timetuple())
169
170
        elapsed_seconds = (end_timestamp - start_timestamp)
171
        instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds)
172
173
    return instance
174
175
176
class ActionBranch(resource.ResourceBranch):
177
178
    def __init__(self, description, app, subparsers, parent_parser=None):
179
        super(ActionBranch, self).__init__(
180
            models.Action, description, app, subparsers,
181
            parent_parser=parent_parser,
182
            commands={
183
                'list': ActionListCommand,
184
                'get': ActionGetCommand,
185
                'update': ActionUpdateCommand,
186
                'delete': ActionDeleteCommand
187
            })
188
189
        # Registers extended commands
190
        self.commands['enable'] = ActionEnableCommand(self.resource, self.app, self.subparsers)
191
        self.commands['disable'] = ActionDisableCommand(self.resource, self.app, self.subparsers)
192
        self.commands['execute'] = ActionRunCommand(
193
            self.resource, self.app, self.subparsers,
194
            add_help=False)
195
196
197
class ActionListCommand(resource.ContentPackResourceListCommand):
198
    display_attributes = ['ref', 'pack', 'description']
199
200
201
class ActionGetCommand(resource.ContentPackResourceGetCommand):
202
    display_attributes = ['all']
203
    attribute_display_order = ['id', 'uid', 'ref', 'pack', 'name', 'description',
204
                               'enabled', 'entry_point', 'runner_type',
205
                               'parameters']
206
207
208
class ActionUpdateCommand(resource.ContentPackResourceUpdateCommand):
209
    pass
210
211
212
class ActionEnableCommand(resource.ContentPackResourceEnableCommand):
213
    display_attributes = ['all']
214
    attribute_display_order = ['id', 'ref', 'pack', 'name', 'description',
215
                               'enabled', 'entry_point', 'runner_type',
216
                               'parameters']
217
218
219
class ActionDisableCommand(resource.ContentPackResourceDisableCommand):
220
    display_attributes = ['all']
221
    attribute_display_order = ['id', 'ref', 'pack', 'name', 'description',
222
                               'enabled', 'entry_point', 'runner_type',
223
                               'parameters']
224
225
226
class ActionDeleteCommand(resource.ContentPackResourceDeleteCommand):
227
    pass
228
229
230
class ActionRunCommandMixin(object):
231
    """
232
    Mixin class which contains utility functions related to action execution.
233
    """
234
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
235
                          'start_timestamp', 'end_timestamp', 'result']
236
    attribute_display_order = ['id', 'action.ref', 'context.user', 'parameters', 'status',
237
                               'start_timestamp', 'end_timestamp', 'result']
238
    attribute_transform_functions = {
239
        'start_timestamp': format_isodate_for_user_timezone,
240
        'end_timestamp': format_isodate_for_user_timezone,
241
        'parameters': format_parameters,
242
        'status': format_status
243
    }
244
245
    poll_interval = 2  # how often to poll for execution completion when using sync mode
246
247
    def get_resource(self, ref_or_id, **kwargs):
248
        return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
249
250
    @add_auth_token_to_kwargs_from_cli
251
    def run_and_print(self, args, **kwargs):
252
        if self._print_help(args, **kwargs):
253
            return
254
255
        execution = self.run(args, **kwargs)
256
        if args.async:
257
            self.print_output('To get the results, execute:\n st2 execution get %s' %
258
                              (execution.id), six.text_type)
259
        else:
260
            self._print_execution_details(execution=execution, args=args, **kwargs)
261
262
        if execution.status == 'failed':
263
            # Exit with non zero if the action has failed
264
            sys.exit(1)
265
266
    def _add_common_options(self):
267
        root_arg_grp = self.parser.add_mutually_exclusive_group()
268
269
        # Display options
270
        task_list_arg_grp = root_arg_grp.add_argument_group()
271
        task_list_arg_grp.add_argument('--raw', action='store_true',
272
                                       help='Raw output, don\'t shot sub-tasks for workflows.')
273
        task_list_arg_grp.add_argument('--show-tasks', action='store_true',
274
                                       help='Whether to show sub-tasks of an execution.')
275
        task_list_arg_grp.add_argument('--depth', type=int, default=-1,
276
                                       help='Depth to which to show sub-tasks. \
277
                                             By default all are shown.')
278
        task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None,
279
                                       help='Set the width of columns in output.')
280
281
        execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group()
282
283
        detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
284
        detail_arg_grp.add_argument('--attr', nargs='+',
285
                                    default=['id', 'status', 'parameters', 'result'],
286
                                    help=('List of attributes to include in the '
287
                                          'output. "all" or unspecified will '
288
                                          'return all attributes.'))
289
        detail_arg_grp.add_argument('-d', '--detail', action='store_true',
290
                                    help='Display full detail of the execution in table format.')
291
292
        result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
293
        result_arg_grp.add_argument('-k', '--key',
294
                                    help=('If result is type of JSON, then print specific '
295
                                          'key-value pair; dot notation for nested JSON is '
296
                                          'supported.'))
297
298
        return root_arg_grp
299
300
    def _print_execution_details(self, execution, args, **kwargs):
301
        """
302
        Print the execution detail to stdout.
303
304
        This method takes into account if an executed action was workflow or not
305
        and formats the output accordingly.
306
        """
307
        runner_type = execution.action.get('runner_type', 'unknown')
308
        is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES
309
310
        show_tasks = getattr(args, 'show_tasks', False)
311
        raw = getattr(args, 'raw', False)
312
        detail = getattr(args, 'detail', False)
313
        key = getattr(args, 'key', None)
314
        attr = getattr(args, 'attr', [])
315
316
        if show_tasks and not is_workflow_action:
317
            raise ValueError('--show-tasks option can only be used with workflow actions')
318
319
        if not raw and not detail and (show_tasks or is_workflow_action):
320
            self._run_and_print_child_task_list(execution=execution, args=args, **kwargs)
321
        else:
322
            instance = execution
323
324
            if detail:
325
                formatter = table.PropertyValueTable
326
            else:
327
                formatter = execution_formatter.ExecutionResult
328
329
            if detail:
330
                options = {'attributes': copy.copy(self.display_attributes)}
331
            elif key:
332
                options = {'attributes': ['result.%s' % (key)], 'key': key}
333
            else:
334
                options = {'attributes': attr}
335
336
            options['json'] = args.json
337
            options['attribute_transform_functions'] = self.attribute_transform_functions
338
            self.print_output(instance, formatter, **options)
339
340
    def _run_and_print_child_task_list(self, execution, args, **kwargs):
341
        action_exec_mgr = self.app.client.managers['LiveAction']
342
343
        instance = execution
344
        options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp',
345
                                  'end_timestamp']}
346
        options['json'] = args.json
347
        options['attribute_transform_functions'] = self.attribute_transform_functions
348
        formatter = execution_formatter.ExecutionResult
349
350
        kwargs['depth'] = args.depth
351
        child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs)
352
        child_instances = self._format_child_instances(child_instances, execution.id)
353
        child_instances = format_execution_statuses(child_instances)
354
355
        if not child_instances:
356
            # No child error, there might be a global error, include result in the output
357
            options['attributes'].append('result')
358
359
        status_index = options['attributes'].index('status')
360
361
        if isinstance(instance.result, dict):
362
            tasks = instance.result.get('tasks', [])
363
        else:
364
            tasks = []
365
366
        # On failure we also want to include error message and traceback at the top level
367
        if instance.status == 'failed':
368
            top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance)
369
370
            if len(tasks) >= 1:
371
                task_error, task_traceback = self._get_task_error(task=tasks[-1])
372
            else:
373
                task_error, task_traceback = None, None
374
375
            if top_level_error:
376
                # Top-level error
377
                instance.error = top_level_error
378
                instance.traceback = top_level_traceback
379
                instance.result = 'See error and traceback.'
380
                options['attributes'].insert(status_index + 1, 'error')
381
                options['attributes'].insert(status_index + 2, 'traceback')
382
            elif task_error:
383
                # Task error
384
                instance.error = task_error
385
                instance.traceback = task_traceback
386
                instance.result = 'See error and traceback.'
387
                instance.failed_on = tasks[-1].get('name', 'unknown')
388
                options['attributes'].insert(status_index + 1, 'error')
389
                options['attributes'].insert(status_index + 2, 'traceback')
390
                options['attributes'].insert(status_index + 3, 'failed_on')
391
392
        # Include result on the top-level object so user doesn't need to issue another command to
393
        # see the result
394
        if len(tasks) >= 1:
395
            task_result = self._get_task_result(task=tasks[-1])
396
397
            if task_result:
398
                instance.result_task = tasks[-1].get('name', 'unknown')
399
                options['attributes'].insert(status_index + 1, 'result_task')
400
                options['attributes'].insert(status_index + 2, 'result')
401
                instance.result = task_result
402
403
        # print root task
404
        self.print_output(instance, formatter, **options)
405
406
        # print child tasks
407
        if child_instances:
408
            self.print_output(child_instances, table.MultiColumnTable,
409
                              attributes=['id', 'status', 'task', 'action', 'start_timestamp'],
410
                              widths=args.width, json=args.json,
411
                              yaml=args.yaml,
412
                              attribute_transform_functions=self.attribute_transform_functions)
413
414
    def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
415
        pending_statuses = [
416
            LIVEACTION_STATUS_REQUESTED,
417
            LIVEACTION_STATUS_SCHEDULED,
418
            LIVEACTION_STATUS_RUNNING,
419
            LIVEACTION_STATUS_CANCELING
420
        ]
421
422
        if not args.async:
423
            while execution.status in pending_statuses:
424
                time.sleep(self.poll_interval)
425
                if not args.json and not args.yaml:
426
                    sys.stdout.write('.')
427
                    sys.stdout.flush()
428
                execution = action_exec_mgr.get_by_id(execution.id, **kwargs)
429
430
            sys.stdout.write('\n')
431
432
            if execution.status == LIVEACTION_STATUS_CANCELED:
433
                return execution
434
435
        return execution
436
437
    def _get_top_level_error(self, live_action):
438
        """
439
        Retrieve a top level workflow error.
440
441
        :return: (error, traceback)
442
        """
443
        if isinstance(live_action.result, dict):
444
            error = live_action.result.get('error', None)
445
            traceback = live_action.result.get('traceback', None)
446
        else:
447
            error = "See result"
448
            traceback = "See result"
449
450
        return error, traceback
451
452
    def _get_task_error(self, task):
453
        """
454
        Retrieve error message from the provided task.
455
456
        :return: (error, traceback)
457
        """
458
        if not task:
459
            return None, None
460
461
        result = task['result']
462
463
        if isinstance(result, dict):
464
            stderr = result.get('stderr', None)
465
            error = result.get('error', None)
466
            traceback = result.get('traceback', None)
467
            error = error if error else stderr
468
        else:
469
            stderr = None
470
            error = None
471
            traceback = None
472
473
        return error, traceback
474
475
    def _get_task_result(self, task):
476
        if not task:
477
            return None
478
479
        return task['result']
480
481
    def _get_action_parameters_from_args(self, action, runner, args):
482
        """
483
        Build a dictionary with parameters which will be passed to the action by
484
        parsing parameters passed to the CLI.
485
486
        :param args: CLI argument.
487
        :type args: ``object``
488
489
        :rtype: ``dict``
490
        """
491
        action_ref_or_id = action.ref
492
493
        def read_file(file_path):
494
            if not os.path.exists(file_path):
495
                raise ValueError('File "%s" doesn\'t exist' % (file_path))
496
497
            if not os.path.isfile(file_path):
498
                raise ValueError('"%s" is not a file' % (file_path))
499
500
            with open(file_path, 'rb') as fp:
501
                content = fp.read()
502
503
            return content
504
505
        def transform_object(value):
506
            # Also support simple key1=val1,key2=val2 syntax
507
            if value.startswith('{'):
508
                # Assume it's JSON
509
                result = value = json.loads(value)
510
            else:
511
                pairs = value.split(',')
512
513
                result = {}
514
                for pair in pairs:
515
                    split = pair.split('=', 1)
516
517
                    if len(split) != 2:
518
                        continue
519
520
                    key, value = split
521
                    result[key] = value
522
            return result
523
524
        def transform_array(value):
525
            # Sometimes an array parameter only has a single element:
526
            #
527
            #     i.e. "st2 run foopack.fooaction arrayparam=51"
528
            #
529
            # Normally, json.loads would throw an exception, and the split method
530
            # would be used. However, since this is an int, not only would
531
            # splitting not work, but json.loads actually treats this as valid JSON,
532
            # but as an int, not an array. This causes a mismatch when the API is called.
533
            #
534
            # We want to try to handle this first, so it doesn't get accidentally
535
            # sent to the API as an int, instead of an array of single-element int.
536
            try:
537
                # Force this to be a list containing the single int, then
538
                # cast the whole thing to string so json.loads can handle it
539
                value = str([int(value)])
540
            except ValueError:
541
                # Original value wasn't an int, so just let it continue
542
                pass
543
544
            # At this point, the input is either a a "json.loads"-able construct
545
            # like [1, 2, 3], or even [1], or it is a comma-separated list,
546
            # Try both, in that order.
547
            try:
548
                result = json.loads(value)
549
            except ValueError:
550
                result = [v.strip() for v in value.split(',')]
551
            return result
552
553
        transformer = {
554
            'array': transform_array,
555
            'boolean': (lambda x: ast.literal_eval(x.capitalize())),
556
            'integer': int,
557
            'number': float,
558
            'object': transform_object,
559
            'string': str
560
        }
561
562
        def normalize(name, value):
563
            """ The desired type is contained in the action meta-data, so we can look that up
564
                and call the desired "caster" function listed in the "transformer" dict
565
            """
566
567
            # Users can also specify type for each array parameter inside an action metadata
568
            # (items: type: int for example) and this information is available here so we could
569
            # also leverage that to cast each array item to the correct type.
570
571
            if name in runner.runner_parameters:
572
                param = runner.runner_parameters[name]
573
                if 'type' in param and param['type'] in transformer:
574
                    return transformer[param['type']](value)
575
576
            if name in action.parameters:
577
                param = action.parameters[name]
578
                if 'type' in param and param['type'] in transformer:
579
                    return transformer[param['type']](value)
580
            return value
581
582
        result = {}
583
584
        if not args.parameters:
585
            return result
586
587
        for idx in range(len(args.parameters)):
588
            arg = args.parameters[idx]
589
            if '=' in arg:
590
                k, v = arg.split('=', 1)
591
592
                # Attribute for files are prefixed with "@"
593
                if k.startswith('@'):
594
                    k = k[1:]
595
                    is_file = True
596
                else:
597
                    is_file = False
598
599
                try:
600
                    if is_file:
601
                        # Files are handled a bit differently since we ship the content
602
                        # over the wire
603
                        file_path = os.path.normpath(pjoin(os.getcwd(), v))
604
                        file_name = os.path.basename(file_path)
605
                        content = read_file(file_path=file_path)
606
607
                        if action_ref_or_id == 'core.http':
608
                            # Special case for http runner
609
                            result['_file_name'] = file_name
610
                            result['file_content'] = content
611
                        else:
612
                            result[k] = content
613
                    else:
614
                        result[k] = normalize(k, v)
615
                except Exception as e:
616
                    # TODO: Move transformers in a separate module and handle
617
                    # exceptions there
618
                    if 'malformed string' in str(e):
619
                        message = ('Invalid value for boolean parameter. '
620
                                   'Valid values are: true, false')
621
                        raise ValueError(message)
622
                    else:
623
                        raise e
624
            else:
625
                result['cmd'] = ' '.join(args.parameters[idx:])
626
                break
627
628
        # Special case for http runner
629
        if 'file_content' in result:
630
            if 'method' not in result:
631
                # Default to POST if a method is not provided
632
                result['method'] = 'POST'
633
634
            if 'file_name' not in result:
635
                # File name not provided, use default file name
636
                result['file_name'] = result['_file_name']
637
638
            del result['_file_name']
639
640
        if args.inherit_env:
641
            result['env'] = self._get_inherited_env_vars()
642
643
        return result
644
645
    @add_auth_token_to_kwargs_from_cli
646
    def _print_help(self, args, **kwargs):
647
        # Print appropriate help message if the help option is given.
648
        action_mgr = self.app.client.managers['Action']
649
        action_exec_mgr = self.app.client.managers['LiveAction']
650
651
        if args.help:
652
            action_ref_or_id = getattr(args, 'ref_or_id', None)
653
            action_exec_id = getattr(args, 'id', None)
654
655
            if action_exec_id and not action_ref_or_id:
656
                action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs)
657
                args.ref_or_id = action_exec.action
658
659
            if action_ref_or_id:
660
                try:
661
                    action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs)
662
                    if not action:
663
                        raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id)
664
                    runner_mgr = self.app.client.managers['RunnerType']
665
                    runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
666
                    parameters, required, optional, _ = self._get_params_types(runner,
667
                                                                               action)
668
                    print('')
669
                    print(textwrap.fill(action.description))
670
                    print('')
671
                    if required:
672
                        required = self._sort_parameters(parameters=parameters,
673
                                                         names=required)
674
675
                        print('Required Parameters:')
676
                        [self._print_param(name, parameters.get(name))
677
                            for name in required]
678
                    if optional:
679
                        optional = self._sort_parameters(parameters=parameters,
680
                                                         names=optional)
681
682
                        print('Optional Parameters:')
683
                        [self._print_param(name, parameters.get(name))
684
                            for name in optional]
685
                except resource.ResourceNotFoundError:
686
                    print(('Action "%s" is not found. ' % args.ref_or_id) +
687
                          'Do "st2 action list" to see list of available actions.')
688
                except Exception as e:
689
                    print('ERROR: Unable to print help for action "%s". %s' %
690
                          (args.ref_or_id, e))
691
            else:
692
                self.parser.print_help()
693
            return True
694
        return False
695
696
    @staticmethod
697
    def _print_param(name, schema):
698
        if not schema:
699
            raise ValueError('Missing schema for parameter "%s"' % (name))
700
701
        wrapper = textwrap.TextWrapper(width=78)
702
        wrapper.initial_indent = ' ' * 4
703
        wrapper.subsequent_indent = wrapper.initial_indent
704
        print(wrapper.fill(name))
705
        wrapper.initial_indent = ' ' * 8
706
        wrapper.subsequent_indent = wrapper.initial_indent
707
        if 'description' in schema and schema['description']:
708
            print(wrapper.fill(schema['description']))
709
        if 'type' in schema and schema['type']:
710
            print(wrapper.fill('Type: %s' % schema['type']))
711
        if 'enum' in schema and schema['enum']:
712
            print(wrapper.fill('Enum: %s' % ', '.join(schema['enum'])))
713
        if 'default' in schema and schema['default'] is not None:
714
            print(wrapper.fill('Default: %s' % schema['default']))
715
        print('')
716
717
    @staticmethod
718
    def _get_params_types(runner, action):
719
        runner_params = runner.runner_parameters
720
        action_params = action.parameters
721
        parameters = copy.copy(runner_params)
722
        parameters.update(copy.copy(action_params))
723
        required = set([k for k, v in six.iteritems(parameters) if v.get('required')])
724
725
        def is_immutable(runner_param_meta, action_param_meta):
726
            # If runner sets a param as immutable, action cannot override that.
727
            if runner_param_meta.get('immutable', False):
728
                return True
729
            else:
730
                return action_param_meta.get('immutable', False)
731
732
        immutable = set()
733
        for param in parameters.keys():
734
            if is_immutable(runner_params.get(param, {}),
735
                            action_params.get(param, {})):
736
                immutable.add(param)
737
738
        required = required - immutable
739
        optional = set(parameters.keys()) - required - immutable
740
741
        return parameters, required, optional, immutable
742
743
    def _format_child_instances(self, children, parent_id):
744
        '''
745
        The goal of this method is to add an indent at every level. This way the
746
        WF is represented as a tree structure while in a list. For the right visuals
747
        representation the list must be a DF traversal else the idents will end up
748
        looking strange.
749
        '''
750
        # apply basic WF formating first.
751
        children = format_wf_instances(children)
752
        # setup a depth lookup table
753
        depth = {parent_id: 0}
754
        result = []
755
        # main loop that indents each entry correctly
756
        for child in children:
757
            # make sure child.parent is in depth and while at it compute the
758
            # right depth for indentation purposes.
759
            if child.parent not in depth:
760
                parent = None
761
                for instance in children:
762
                    if WF_PREFIX in instance.id:
763
                        instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):]
764
                    else:
765
                        instance_id = instance.id
766
                    if instance_id == child.parent:
767
                        parent = instance
768
                if parent and parent.parent and parent.parent in depth:
769
                    depth[child.parent] = depth[parent.parent] + 1
770
                else:
771
                    depth[child.parent] = 0
772
            # now ident for the right visuals
773
            child.id = INDENT_CHAR * depth[child.parent] + child.id
774
            result.append(self._format_for_common_representation(child))
775
        return result
776
777
    def _format_for_common_representation(self, task):
778
        '''
779
        Formats a task for common representation between mistral and action-chain.
780
        '''
781
        # This really needs to be better handled on the back-end but that would be a bigger
782
        # change so handling in cli.
783
        context = getattr(task, 'context', None)
784
        if context and 'chain' in context:
785
            task_name_key = 'context.chain.name'
786
        elif context and 'mistral' in context:
787
            task_name_key = 'context.mistral.task_name'
788
        # Use LiveAction as the object so that the formatter lookup does not change.
789
        # AKA HACK!
790
        return models.action.LiveAction(**{
791
            'id': task.id,
792
            'status': task.status,
793
            'task': jsutil.get_value(vars(task), task_name_key),
794
            'action': task.action.get('ref', None),
795
            'start_timestamp': task.start_timestamp,
796
            'end_timestamp': getattr(task, 'end_timestamp', None)
797
        })
798
799
    def _sort_parameters(self, parameters, names):
800
        """
801
        Sort a provided list of action parameters.
802
803
        :type parameters: ``list``
804
        :type names: ``list`` or ``set``
805
        """
806
        sorted_parameters = sorted(names, key=lambda name:
807
                                   self._get_parameter_sort_value(
808
                                       parameters=parameters,
809
                                       name=name))
810
811
        return sorted_parameters
812
813
    def _get_parameter_sort_value(self, parameters, name):
814
        """
815
        Return a value which determines sort order for a particular parameter.
816
817
        By default, parameters are sorted using "position" parameter attribute.
818
        If this attribute is not available, parameter is sorted based on the
819
        name.
820
        """
821
        parameter = parameters.get(name, None)
822
823
        if not parameter:
824
            return None
825
826
        sort_value = parameter.get('position', name)
827
        return sort_value
828
829
    def _get_inherited_env_vars(self):
830
        env_vars = os.environ.copy()
831
832
        for var_name in ENV_VARS_BLACKLIST:
833
            if var_name.lower() in env_vars:
834
                del env_vars[var_name.lower()]
835
            if var_name.upper() in env_vars:
836
                del env_vars[var_name.upper()]
837
838
        return env_vars
839
840
841
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
842
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
843
844
        super(ActionRunCommand, self).__init__(
845
            resource, kwargs.pop('name', 'execute'),
846
            'A command to invoke an action manually.',
847
            *args, **kwargs)
848
849
        self.parser.add_argument('ref_or_id', nargs='?',
850
                                 metavar='ref-or-id',
851
                                 help='Action reference (pack.action_name) ' +
852
                                 'or ID of the action.')
853
        self.parser.add_argument('parameters', nargs='*',
854
                                 help='List of keyword args, positional args, '
855
                                      'and optional args for the action.')
856
857
        self.parser.add_argument('-h', '--help',
858
                                 action='store_true', dest='help',
859
                                 help='Print usage for the given action.')
860
861
        self._add_common_options()
862
863
        if self.name in ['run', 'execute']:
864
            self.parser.add_argument('--trace-tag', '--trace_tag',
865
                                     help='A trace tag string to track execution later.',
866
                                     dest='trace_tag', required=False)
867
            self.parser.add_argument('--trace-id',
868
                                     help='Existing trace id for this execution.',
869
                                     dest='trace_id', required=False)
870
            self.parser.add_argument('-a', '--async',
871
                                     action='store_true', dest='async',
872
                                     help='Do not wait for action to finish.')
873
            self.parser.add_argument('-e', '--inherit-env',
874
                                     action='store_true', dest='inherit_env',
875
                                     help='Pass all the environment variables '
876
                                          'which are accessible to the CLI as "env" '
877
                                          'parameter to the action. Note: Only works '
878
                                          'with python, local and remote runners.')
879
            self.parser.add_argument('-u', '--user', type=str, default=None,
880
                                           help='User under which to run the action (admins only).')
881
882
        if self.name == 'run':
883
            self.parser.set_defaults(async=False)
884
        else:
885
            self.parser.set_defaults(async=True)
886
887
    @add_auth_token_to_kwargs_from_cli
888
    def run(self, args, **kwargs):
889
        if not args.ref_or_id:
890
            self.parser.error('Missing action reference or id')
891
892
        action = self.get_resource(args.ref_or_id, **kwargs)
893
        if not action:
894
            raise resource.ResourceNotFoundError('Action "%s" cannot be found.'
895
                                                 % (args.ref_or_id))
896
897
        runner_mgr = self.app.client.managers['RunnerType']
898
        runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
899
        if not runner:
900
            raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.'
901
                                                 % (action.runner_type, action.name))
902
903
        action_ref = '.'.join([action.pack, action.name])
904
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
905
                                                                  args=args)
906
907
        execution = models.LiveAction()
908
        execution.action = action_ref
909
        execution.parameters = action_parameters
910
        execution.user = args.user
911
912
        if not args.trace_id and args.trace_tag:
913
            execution.context = {'trace_context': {'trace_tag': args.trace_tag}}
914
915
        if args.trace_id:
916
            execution.context = {'trace_context': {'id_': args.trace_id}}
917
918
        action_exec_mgr = self.app.client.managers['LiveAction']
919
920
        execution = action_exec_mgr.create(execution, **kwargs)
921
        execution = self._get_execution_result(execution=execution,
922
                                               action_exec_mgr=action_exec_mgr,
923
                                               args=args, **kwargs)
924
        return execution
925
926
927
class ActionExecutionBranch(resource.ResourceBranch):
928
929
    def __init__(self, description, app, subparsers, parent_parser=None):
930
        super(ActionExecutionBranch, self).__init__(
931
            models.LiveAction, description, app, subparsers,
932
            parent_parser=parent_parser, read_only=True,
933
            commands={'list': ActionExecutionListCommand,
934
                      'get': ActionExecutionGetCommand})
935
936
        # Register extended commands
937
        self.commands['re-run'] = ActionExecutionReRunCommand(self.resource, self.app,
938
                                                              self.subparsers, add_help=False)
939
        self.commands['cancel'] = ActionExecutionCancelCommand(self.resource, self.app,
940
                                                               self.subparsers, add_help=False)
941
942
943
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled')
944
945
946
class ActionExecutionReadCommand(resource.ResourceCommand):
947
    """
948
    Base class for read / view commands (list and get).
949
    """
950
951
    @classmethod
952
    def _get_exclude_attributes(cls, args):
953
        """
954
        Retrieve a list of exclude attributes for particular command line arguments.
955
        """
956
        exclude_attributes = []
957
958
        result_included = False
959
        trigger_instance_included = False
960
961
        for attr in args.attr:
962
            # Note: We perform startswith check so we correctly detected child attribute properties
963
            # (e.g. result, result.stdout, result.stderr, etc.)
964
            if attr.startswith('result'):
965
                result_included = True
966
967
            if attr.startswith('trigger_instance'):
968
                trigger_instance_included = True
969
970
        if not result_included:
971
            exclude_attributes.append('result')
972
        if not trigger_instance_included:
973
            exclude_attributes.append('trigger_instance')
974
975
        return exclude_attributes
976
977
978
class ActionExecutionListCommand(ActionExecutionReadCommand):
979
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
980
                          'end_timestamp']
981
    attribute_transform_functions = {
982
        'start_timestamp': format_isodate_for_user_timezone,
983
        'end_timestamp': format_isodate_for_user_timezone,
984
        'parameters': format_parameters,
985
        'status': format_status
986
    }
987
988
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
989
        super(ActionExecutionListCommand, self).__init__(
990
            resource, 'list', 'Get the list of the 50 most recent %s.' %
991
            resource.get_plural_display_name().lower(),
992
            *args, **kwargs)
993
994
        self.group = self.parser.add_argument_group()
995
        self.parser.add_argument('-n', '--last', type=int, dest='last',
996
                                 default=50,
997
                                 help=('List N most recent %s.' %
998
                                       resource.get_plural_display_name().lower()))
999
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
1000
                                 default='descending',
1001
                                 help=('Sort %s by start timestamp, '
1002
                                       'asc|ascending (earliest first) '
1003
                                       'or desc|descending (latest first)' %
1004
                                       resource.get_plural_display_name().lower()))
1005
1006
        # Filter options
1007
        self.group.add_argument('--action', help='Action reference to filter the list.')
1008
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
1009
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
1010
                                                  '\'%s\' or \'%s\''
1011
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
1012
        self.group.add_argument('--trigger_instance',
1013
                                help='Trigger instance id to filter the list.')
1014
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
1015
                                 default=None,
1016
                                 help=('Only return executions with timestamp '
1017
                                       'greater than the one provided. '
1018
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1019
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
1020
                                 default=None,
1021
                                 help=('Only return executions with timestamp '
1022
                                       'lower than the one provided. '
1023
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1024
        self.parser.add_argument('-l', '--showall', action='store_true',
1025
                                 help='')
1026
1027
        # Display options
1028
        self.parser.add_argument('-a', '--attr', nargs='+',
1029
                                 default=self.display_attributes,
1030
                                 help=('List of attributes to include in the '
1031
                                       'output. "all" will return all '
1032
                                       'attributes.'))
1033
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
1034
                                 default=None,
1035
                                 help=('Set the width of columns in output.'))
1036
1037
    @add_auth_token_to_kwargs_from_cli
1038
    def run(self, args, **kwargs):
1039
        # Filtering options
1040
        if args.action:
1041
            kwargs['action'] = args.action
1042
        if args.status:
1043
            kwargs['status'] = args.status
1044
        if args.trigger_instance:
1045
            kwargs['trigger_instance'] = args.trigger_instance
1046
        if not args.showall:
1047
            # null is the magic string that translates to does not exist.
1048
            kwargs['parent'] = 'null'
1049
        if args.timestamp_gt:
1050
            kwargs['timestamp_gt'] = args.timestamp_gt
1051
        if args.timestamp_lt:
1052
            kwargs['timestamp_lt'] = args.timestamp_lt
1053
        if args.sort_order:
1054
            if args.sort_order in ['asc', 'ascending']:
1055
                kwargs['sort_asc'] = True
1056
            elif args.sort_order in ['desc', 'descending']:
1057
                kwargs['sort_desc'] = True
1058
1059
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
1060
        # since they are not displayed nor used which speeds the common operation substantially.
1061
        exclude_attributes = self._get_exclude_attributes(args=args)
1062
        exclude_attributes = ','.join(exclude_attributes)
1063
        kwargs['exclude_attributes'] = exclude_attributes
1064
1065
        return self.manager.query(limit=args.last, **kwargs)
1066
1067
    def run_and_print(self, args, **kwargs):
1068
        instances = format_wf_instances(self.run(args, **kwargs))
1069
1070
        if not args.json and not args.yaml:
1071
            # Include elapsed time for running executions
1072
            instances = format_execution_statuses(instances)
1073
1074
        self.print_output(reversed(instances), table.MultiColumnTable,
1075
                          attributes=args.attr, widths=args.width,
1076
                          json=args.json,
1077
                          yaml=args.yaml,
1078
                          attribute_transform_functions=self.attribute_transform_functions)
1079
1080
1081
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1082
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1083
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1084
1085
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1086
        super(ActionExecutionGetCommand, self).__init__(
1087
            resource, 'get',
1088
            'Get individual %s.' % resource.get_display_name().lower(),
1089
            *args, **kwargs)
1090
1091
        self.parser.add_argument('id',
1092
                                 help=('ID of the %s.' %
1093
                                       resource.get_display_name().lower()))
1094
1095
        self._add_common_options()
1096
1097
    @add_auth_token_to_kwargs_from_cli
1098
    def run(self, args, **kwargs):
1099
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1100
        # requested by user either via "--attr" flag or by default.
1101
        exclude_attributes = self._get_exclude_attributes(args=args)
1102
        exclude_attributes = ','.join(exclude_attributes)
1103
1104
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1105
1106
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1107
        return execution
1108
1109
    @add_auth_token_to_kwargs_from_cli
1110
    def run_and_print(self, args, **kwargs):
1111
        try:
1112
            execution = self.run(args, **kwargs)
1113
1114
            if not args.json and not args.yaml:
1115
                # Include elapsed time for running executions
1116
                execution = format_execution_status(execution)
1117
        except resource.ResourceNotFoundError:
1118
            self.print_not_found(args.id)
1119
            raise OperationFailureException('Execution %s not found.' % (args.id))
1120
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1121
1122
1123
class ActionExecutionCancelCommand(resource.ResourceCommand):
1124
1125
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1126
        super(ActionExecutionCancelCommand, self).__init__(
1127
            resource, 'cancel', 'Cancel %s.' %
1128
            resource.get_plural_display_name().lower(),
1129
            *args, **kwargs)
1130
1131
        self.parser.add_argument('ids',
1132
                                 nargs='+',
1133
                                 help=('IDs of the %ss to cancel.' %
1134
                                       resource.get_display_name().lower()))
1135
1136
    def run(self, args, **kwargs):
1137
        responses = []
1138
        for execution_id in args.ids:
1139
            response = self.manager.delete_by_id(execution_id)
1140
            responses.append([execution_id, response])
1141
1142
        return responses
1143
1144
    @add_auth_token_to_kwargs_from_cli
1145
    def run_and_print(self, args, **kwargs):
1146
        responses = self.run(args, **kwargs)
1147
1148
        for execution_id, response in responses:
1149
            self._print_result(execution_id=execution_id, response=response)
1150
1151
    def _print_result(self, execution_id, response):
1152
        if response and 'faultstring' in response:
1153
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1154
                                   (self.resource.get_display_name().lower(), execution_id))
1155
1156
        elif response:
1157
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1158
                                                   execution_id)
1159
        else:
1160
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1161
                                                        execution_id)
1162
        print(message)
1163
1164
1165
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1166
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1167
1168
        super(ActionExecutionReRunCommand, self).__init__(
1169
            resource, kwargs.pop('name', 're-run'),
1170
            'A command to re-run a particular action.',
1171
            *args, **kwargs)
1172
1173
        self.parser.add_argument('id', nargs='?',
1174
                                 metavar='id',
1175
                                 help='ID of action execution to re-run ')
1176
        self.parser.add_argument('parameters', nargs='*',
1177
                                 help='List of keyword args, positional args, '
1178
                                      'and optional args for the action.')
1179
        self.parser.add_argument('--tasks', nargs='*',
1180
                                 help='Name of the workflow tasks to re-run.')
1181
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1182
                                 help='Name of the with-items tasks to not reset. This only '
1183
                                      'applies to Mistral workflows. By default, all iterations '
1184
                                      'for with-items tasks is rerun. If no reset, only failed '
1185
                                      ' iterations are rerun.')
1186
        self.parser.add_argument('-a', '--async',
1187
                                 action='store_true', dest='async',
1188
                                 help='Do not wait for action to finish.')
1189
        self.parser.add_argument('-e', '--inherit-env',
1190
                                 action='store_true', dest='inherit_env',
1191
                                 help='Pass all the environment variables '
1192
                                      'which are accessible to the CLI as "env" '
1193
                                      'parameter to the action. Note: Only works '
1194
                                      'with python, local and remote runners.')
1195
        self.parser.add_argument('-h', '--help',
1196
                                 action='store_true', dest='help',
1197
                                 help='Print usage for the given action.')
1198
1199
        self._add_common_options()
1200
1201
    @add_auth_token_to_kwargs_from_cli
1202
    def run(self, args, **kwargs):
1203
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1204
1205
        if not existing_execution:
1206
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1207
                                                 (args.id))
1208
1209
        action_mgr = self.app.client.managers['Action']
1210
        runner_mgr = self.app.client.managers['RunnerType']
1211
        action_exec_mgr = self.app.client.managers['LiveAction']
1212
1213
        action_ref = existing_execution.action['ref']
1214
        action = action_mgr.get_by_ref_or_id(action_ref)
1215
        runner = runner_mgr.get_by_name(action.runner_type)
1216
1217
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1218
                                                                  args=args)
1219
1220
        execution = action_exec_mgr.re_run(execution_id=args.id,
1221
                                           parameters=action_parameters,
1222
                                           tasks=args.tasks,
1223
                                           no_reset=args.no_reset,
1224
                                           **kwargs)
1225
1226
        execution = self._get_execution_result(execution=execution,
1227
                                               action_exec_mgr=action_exec_mgr,
1228
                                               args=args, **kwargs)
1229
1230
        return execution
1231