Passed
Pull Request — master (#3640)
by Lakshmi
06:19
created

ActionRunCommandMixin.transform_array()   F

Complexity

Conditions 11

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 11
c 4
b 0
f 0
dl 0
loc 44
rs 3.1764

How to fix   Complexity   

Complexity

Complex classes like ActionRunCommandMixin.transform_array() 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
LIVEACTION_STATUS_PAUSING = 'pausing'
53
LIVEACTION_STATUS_PAUSED = 'paused'
54
LIVEACTION_STATUS_RESUMING = 'resuming'
55
56
LIVEACTION_COMPLETED_STATES = [
57
    LIVEACTION_STATUS_SUCCEEDED,
58
    LIVEACTION_STATUS_FAILED,
59
    LIVEACTION_STATUS_TIMED_OUT,
60
    LIVEACTION_STATUS_CANCELED,
61
    LIVEACTION_STATUS_ABANDONED
62
]
63
64
# Who parameters should be masked when displaying action execution output
65
PARAMETERS_TO_MASK = [
66
    'password',
67
    'private_key'
68
]
69
70
# A list of environment variables which are never inherited when using run
71
# --inherit-env flag
72
ENV_VARS_BLACKLIST = [
73
    'pwd',
74
    'mail',
75
    'username',
76
    'user',
77
    'path',
78
    'home',
79
    'ps1',
80
    'shell',
81
    'pythonpath',
82
    'ssh_tty',
83
    'ssh_connection',
84
    'lang',
85
    'ls_colors',
86
    'logname',
87
    'oldpwd',
88
    'term',
89
    'xdg_session_id'
90
]
91
92
WORKFLOW_RUNNER_TYPES = [
93
    'action-chain',
94
    'mistral-v2',
95
]
96
97
98
def format_parameters(value):
99
    # Mask sensitive parameters
100
    if not isinstance(value, dict):
101
        # No parameters, leave it as it is
102
        return value
103
104
    for param_name, _ in value.items():
105
        if param_name in PARAMETERS_TO_MASK:
106
            value[param_name] = '********'
107
108
    return value
109
110
111
# String for indenting etc.
112
WF_PREFIX = '+ '
113
NON_WF_PREFIX = '  '
114
INDENT_CHAR = ' '
115
116
117
def format_wf_instances(instances):
118
    """
119
    Adds identification characters to a workflow and appropriately shifts
120
    the non-workflow instances. If no workflows are found does nothing.
121
    """
122
    # only add extr chars if there are workflows.
123
    has_wf = False
124
    for instance in instances:
125
        if not getattr(instance, 'children', None):
126
            continue
127
        else:
128
            has_wf = True
129
            break
130
    if not has_wf:
131
        return instances
132
    # Prepend wf and non_wf prefixes.
133
    for instance in instances:
134
        if getattr(instance, 'children', None):
135
            instance.id = WF_PREFIX + instance.id
136
        else:
137
            instance.id = NON_WF_PREFIX + instance.id
138
    return instances
139
140
141
def format_execution_statuses(instances):
142
    result = []
143
    for instance in instances:
144
        instance = format_execution_status(instance)
145
        result.append(instance)
146
147
    return result
148
149
150
def format_execution_status(instance):
151
    """
152
    Augment instance "status" attribute with number of seconds which have elapsed for all the
153
    executions which are in running state and execution total run time for all the executions
154
    which have finished.
155
    """
156
    start_timestamp = getattr(instance, 'start_timestamp', None)
157
    end_timestamp = getattr(instance, 'end_timestamp', None)
158
159
    if instance.status == LIVEACTION_STATUS_RUNNING and start_timestamp:
160
        start_timestamp = instance.start_timestamp
161
        start_timestamp = parse_isotime(start_timestamp)
162
        start_timestamp = calendar.timegm(start_timestamp.timetuple())
163
        now = int(time.time())
164
        elapsed_seconds = (now - start_timestamp)
165
        instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds)
166
    elif instance.status in LIVEACTION_COMPLETED_STATES and start_timestamp and end_timestamp:
167
        start_timestamp = parse_isotime(start_timestamp)
168
        start_timestamp = calendar.timegm(start_timestamp.timetuple())
169
        end_timestamp = parse_isotime(end_timestamp)
170
        end_timestamp = calendar.timegm(end_timestamp.timetuple())
171
172
        elapsed_seconds = (end_timestamp - start_timestamp)
173
        instance.status = '%s (%ss elapsed)' % (instance.status, elapsed_seconds)
174
175
    return instance
176
177
178
class ActionBranch(resource.ResourceBranch):
179
180
    def __init__(self, description, app, subparsers, parent_parser=None):
181
        super(ActionBranch, self).__init__(
182
            models.Action, description, app, subparsers,
183
            parent_parser=parent_parser,
184
            commands={
185
                'list': ActionListCommand,
186
                'get': ActionGetCommand,
187
                'update': ActionUpdateCommand,
188
                'delete': ActionDeleteCommand
189
            })
190
191
        # Registers extended commands
192
        self.commands['enable'] = ActionEnableCommand(self.resource, self.app, self.subparsers)
193
        self.commands['disable'] = ActionDisableCommand(self.resource, self.app, self.subparsers)
194
        self.commands['execute'] = ActionRunCommand(
195
            self.resource, self.app, self.subparsers,
196
            add_help=False)
197
198
199
class ActionListCommand(resource.ContentPackResourceListCommand):
200
    display_attributes = ['ref', 'pack', 'description']
201
202
203
class ActionGetCommand(resource.ContentPackResourceGetCommand):
204
    display_attributes = ['all']
205
    attribute_display_order = ['id', 'uid', 'ref', 'pack', 'name', 'description',
206
                               'enabled', 'entry_point', 'runner_type',
207
                               'parameters']
208
209
210
class ActionUpdateCommand(resource.ContentPackResourceUpdateCommand):
211
    pass
212
213
214
class ActionEnableCommand(resource.ContentPackResourceEnableCommand):
215
    display_attributes = ['all']
216
    attribute_display_order = ['id', 'ref', 'pack', 'name', 'description',
217
                               'enabled', 'entry_point', 'runner_type',
218
                               'parameters']
219
220
221
class ActionDisableCommand(resource.ContentPackResourceDisableCommand):
222
    display_attributes = ['all']
223
    attribute_display_order = ['id', 'ref', 'pack', 'name', 'description',
224
                               'enabled', 'entry_point', 'runner_type',
225
                               'parameters']
226
227
228
class ActionDeleteCommand(resource.ContentPackResourceDeleteCommand):
229
    pass
230
231
232
class ActionRunCommandMixin(object):
233
    """
234
    Mixin class which contains utility functions related to action execution.
235
    """
236
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
237
                          'start_timestamp', 'end_timestamp', 'result']
238
    attribute_display_order = ['id', 'action.ref', 'context.user', 'parameters', 'status',
239
                               'start_timestamp', 'end_timestamp', 'result']
240
    attribute_transform_functions = {
241
        'start_timestamp': format_isodate_for_user_timezone,
242
        'end_timestamp': format_isodate_for_user_timezone,
243
        'parameters': format_parameters,
244
        'status': format_status
245
    }
246
247
    poll_interval = 2  # how often to poll for execution completion when using sync mode
248
249
    def get_resource(self, ref_or_id, **kwargs):
250
        return self.get_resource_by_ref_or_id(ref_or_id=ref_or_id, **kwargs)
251
252
    @add_auth_token_to_kwargs_from_cli
253
    def run_and_print(self, args, **kwargs):
254
        if self._print_help(args, **kwargs):
255
            return
256
257
        execution = self.run(args, **kwargs)
258
        if args.async:
259
            self.print_output('To get the results, execute:\n st2 execution get %s' %
260
                              (execution.id), six.text_type)
261
        else:
262
            self._print_execution_details(execution=execution, args=args, **kwargs)
263
264
        if execution.status == 'failed':
265
            # Exit with non zero if the action has failed
266
            sys.exit(1)
267
268
    def _add_common_options(self):
269
        root_arg_grp = self.parser.add_mutually_exclusive_group()
270
271
        # Display options
272
        task_list_arg_grp = root_arg_grp.add_argument_group()
273
        task_list_arg_grp.add_argument('--raw', action='store_true',
274
                                       help='Raw output, don\'t shot sub-tasks for workflows.')
275
        task_list_arg_grp.add_argument('--show-tasks', action='store_true',
276
                                       help='Whether to show sub-tasks of an execution.')
277
        task_list_arg_grp.add_argument('--depth', type=int, default=-1,
278
                                       help='Depth to which to show sub-tasks. \
279
                                             By default all are shown.')
280
        task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None,
281
                                       help='Set the width of columns in output.')
282
283
        execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group()
284
285
        detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
286
        detail_arg_grp.add_argument('--attr', nargs='+',
287
                                    default=['id', 'status', 'parameters', 'result'],
288
                                    help=('List of attributes to include in the '
289
                                          'output. "all" or unspecified will '
290
                                          'return all attributes.'))
291
        detail_arg_grp.add_argument('-d', '--detail', action='store_true',
292
                                    help='Display full detail of the execution in table format.')
293
294
        result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
295
        result_arg_grp.add_argument('-k', '--key',
296
                                    help=('If result is type of JSON, then print specific '
297
                                          'key-value pair; dot notation for nested JSON is '
298
                                          'supported.'))
299
300
        return root_arg_grp
301
302
    def _print_execution_details(self, execution, args, **kwargs):
303
        """
304
        Print the execution detail to stdout.
305
306
        This method takes into account if an executed action was workflow or not
307
        and formats the output accordingly.
308
        """
309
        runner_type = execution.action.get('runner_type', 'unknown')
310
        is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES
311
312
        show_tasks = getattr(args, 'show_tasks', False)
313
        raw = getattr(args, 'raw', False)
314
        detail = getattr(args, 'detail', False)
315
        key = getattr(args, 'key', None)
316
        attr = getattr(args, 'attr', [])
317
318
        if show_tasks and not is_workflow_action:
319
            raise ValueError('--show-tasks option can only be used with workflow actions')
320
321
        if not raw and not detail and (show_tasks or is_workflow_action):
322
            self._run_and_print_child_task_list(execution=execution, args=args, **kwargs)
323
        else:
324
            instance = execution
325
326
            if detail:
327
                formatter = table.PropertyValueTable
328
            else:
329
                formatter = execution_formatter.ExecutionResult
330
331
            if detail:
332
                options = {'attributes': copy.copy(self.display_attributes)}
333
            elif key:
334
                options = {'attributes': ['result.%s' % (key)], 'key': key}
335
            else:
336
                options = {'attributes': attr}
337
338
            options['json'] = args.json
339
            options['attribute_transform_functions'] = self.attribute_transform_functions
340
            self.print_output(instance, formatter, **options)
341
342
    def _run_and_print_child_task_list(self, execution, args, **kwargs):
343
        action_exec_mgr = self.app.client.managers['LiveAction']
344
345
        instance = execution
346
        options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp',
347
                                  'end_timestamp']}
348
        options['json'] = args.json
349
        options['attribute_transform_functions'] = self.attribute_transform_functions
350
        formatter = execution_formatter.ExecutionResult
351
352
        kwargs['depth'] = args.depth
353
        child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs)
354
        child_instances = self._format_child_instances(child_instances, execution.id)
355
        child_instances = format_execution_statuses(child_instances)
356
357
        if not child_instances:
358
            # No child error, there might be a global error, include result in the output
359
            options['attributes'].append('result')
360
361
        status_index = options['attributes'].index('status')
362
363
        if hasattr(instance, 'result') and isinstance(instance.result, dict):
364
            tasks = instance.result.get('tasks', [])
365
        else:
366
            tasks = []
367
368
        # On failure we also want to include error message and traceback at the top level
369
        if instance.status == 'failed':
370
            top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance)
371
372
            if len(tasks) >= 1:
373
                task_error, task_traceback = self._get_task_error(task=tasks[-1])
374
            else:
375
                task_error, task_traceback = None, None
376
377
            if top_level_error:
378
                # Top-level error
379
                instance.error = top_level_error
380
                instance.traceback = top_level_traceback
381
                instance.result = 'See error and traceback.'
382
                options['attributes'].insert(status_index + 1, 'error')
383
                options['attributes'].insert(status_index + 2, 'traceback')
384
            elif task_error:
385
                # Task error
386
                instance.error = task_error
387
                instance.traceback = task_traceback
388
                instance.result = 'See error and traceback.'
389
                instance.failed_on = tasks[-1].get('name', 'unknown')
390
                options['attributes'].insert(status_index + 1, 'error')
391
                options['attributes'].insert(status_index + 2, 'traceback')
392
                options['attributes'].insert(status_index + 3, 'failed_on')
393
394
        # Include result on the top-level object so user doesn't need to issue another command to
395
        # see the result
396
        if len(tasks) >= 1:
397
            task_result = self._get_task_result(task=tasks[-1])
398
399
            if task_result:
400
                instance.result_task = tasks[-1].get('name', 'unknown')
401
                options['attributes'].insert(status_index + 1, 'result_task')
402
                options['attributes'].insert(status_index + 2, 'result')
403
                instance.result = task_result
404
405
        # print root task
406
        self.print_output(instance, formatter, **options)
407
408
        # print child tasks
409
        if child_instances:
410
            self.print_output(child_instances, table.MultiColumnTable,
411
                              attributes=['id', 'status', 'task', 'action', 'start_timestamp'],
412
                              widths=args.width, json=args.json,
413
                              yaml=args.yaml,
414
                              attribute_transform_functions=self.attribute_transform_functions)
415
416
    def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
417
        pending_statuses = [
418
            LIVEACTION_STATUS_REQUESTED,
419
            LIVEACTION_STATUS_SCHEDULED,
420
            LIVEACTION_STATUS_RUNNING,
421
            LIVEACTION_STATUS_CANCELING
422
        ]
423
424
        if not args.async:
425
            while execution.status in pending_statuses:
426
                time.sleep(self.poll_interval)
427
                if not args.json and not args.yaml:
428
                    sys.stdout.write('.')
429
                    sys.stdout.flush()
430
                execution = action_exec_mgr.get_by_id(execution.id, **kwargs)
431
432
            sys.stdout.write('\n')
433
434
            if execution.status == LIVEACTION_STATUS_CANCELED:
435
                return execution
436
437
        return execution
438
439
    def _get_top_level_error(self, live_action):
440
        """
441
        Retrieve a top level workflow error.
442
443
        :return: (error, traceback)
444
        """
445
        if isinstance(live_action.result, dict):
446
            error = live_action.result.get('error', None)
447
            traceback = live_action.result.get('traceback', None)
448
        else:
449
            error = "See result"
450
            traceback = "See result"
451
452
        return error, traceback
453
454
    def _get_task_error(self, task):
455
        """
456
        Retrieve error message from the provided task.
457
458
        :return: (error, traceback)
459
        """
460
        if not task:
461
            return None, None
462
463
        result = task['result']
464
465
        if isinstance(result, dict):
466
            stderr = result.get('stderr', None)
467
            error = result.get('error', None)
468
            traceback = result.get('traceback', None)
469
            error = error if error else stderr
470
        else:
471
            stderr = None
472
            error = None
473
            traceback = None
474
475
        return error, traceback
476
477
    def _get_task_result(self, task):
478
        if not task:
479
            return None
480
481
        return task['result']
482
483
    def _get_action_parameters_from_args(self, action, runner, args):
484
        """
485
        Build a dictionary with parameters which will be passed to the action by
486
        parsing parameters passed to the CLI.
487
488
        :param args: CLI argument.
489
        :type args: ``object``
490
491
        :rtype: ``dict``
492
        """
493
        action_ref_or_id = action.ref
494
495
        def read_file(file_path):
496
            if not os.path.exists(file_path):
497
                raise ValueError('File "%s" doesn\'t exist' % (file_path))
498
499
            if not os.path.isfile(file_path):
500
                raise ValueError('"%s" is not a file' % (file_path))
501
502
            with open(file_path, 'rb') as fp:
503
                content = fp.read()
504
505
            return content
506
507
        def transform_object(value):
508
            # Also support simple key1=val1,key2=val2 syntax
509
            if value.startswith('{'):
510
                # Assume it's JSON
511
                result = value = json.loads(value)
512
            else:
513
                pairs = value.split(',')
514
515
                result = {}
516
                for pair in pairs:
517
                    split = pair.split('=', 1)
518
519
                    if len(split) != 2:
520
                        continue
521
522
                    key, value = split
523
                    result[key] = value
524
            return result
525
526
        def transform_array(value, action_params=None):
527
            action_params = action_params or {}
528
529
            # Sometimes an array parameter only has a single element:
530
            #
531
            #     i.e. "st2 run foopack.fooaction arrayparam=51"
532
            #
533
            # Normally, json.loads would throw an exception, and the split method
534
            # would be used. However, since this is an int, not only would
535
            # splitting not work, but json.loads actually treats this as valid JSON,
536
            # but as an int, not an array. This causes a mismatch when the API is called.
537
            #
538
            # We want to try to handle this first, so it doesn't get accidentally
539
            # sent to the API as an int, instead of an array of single-element int.
540
            try:
541
                # Force this to be a list containing the single int, then
542
                # cast the whole thing to string so json.loads can handle it
543
                value = str([int(value)])
544
            except ValueError:
545
                # Original value wasn't an int, so just let it continue
546
                pass
547
548
            # At this point, the input is either a a "json.loads"-able construct
549
            # like [1, 2, 3], or even [1], or it is a comma-separated list,
550
            # Try both, in that order.
551
            try:
552
                result = json.loads(value)
553
            except ValueError:
554
                result = [v.strip() for v in value.split(',')]
555
556
            # When each values in this array represent dict type, this converts
557
            # the 'result' to the dict type value.
558
            if all([isinstance(x, str) and ':' in x for x in result]):
559
                result_dict = {}
560
                for (k, v) in [x.split(':') for x in result]:
561
                    # To parse values using the 'transformer' according to the type which is
562
                    # specified in the action metadata, calling 'normalize' method recursively.
563
                    if 'properties' in action_params and k in action_params['properties']:
564
                        result_dict[k] = normalize(k, v, action_params['properties'])
565
                    else:
566
                        result_dict[k] = v
567
                return [result_dict]
568
569
            return result
570
571
        transformer = {
572
            'array': transform_array,
573
            'boolean': (lambda x: ast.literal_eval(x.capitalize())),
574
            'integer': int,
575
            'number': float,
576
            'object': transform_object,
577
            'string': str
578
        }
579
580
        def get_param_type(key, action_params=None):
581
            action_params = action_params or action.parameters
582
583
            param = None
584
            if key in runner.runner_parameters:
585
                param = runner.runner_parameters[key]
586
            elif key in action_params:
587
                param = action_params[key]
588
589
            if param:
590
                return param['type']
591
592
            return None
593
594
        def normalize(name, value, action_params=None):
595
            """ The desired type is contained in the action meta-data, so we can look that up
596
                and call the desired "caster" function listed in the "transformer" dict
597
            """
598
            action_params = action_params or action.parameters
599
600
            # By default, this method uses a parameter which is defined in the action metadata.
601
            # This method assume to be called recursively for parsing values in an array of objects
602
            # type value according to the nested action metadata definition.
603
            #
604
            # This is a best practice to pass a list value as default argument to prevent
605
            # unforeseen consequence by being created a persistent object.
606
607
            # Users can also specify type for each array parameter inside an action metadata
608
            # (items: type: int for example) and this information is available here so we could
609
            # also leverage that to cast each array item to the correct type.
610
            param_type = get_param_type(name, action_params)
611
            if param_type == 'array' and name in action_params:
612
                return transformer[param_type](value, action_params[name])
613
            elif param_type:
614
                return transformer[param_type](value)
615
616
            return value
617
618
        result = {}
619
620
        if not args.parameters:
621
            return result
622
623
        for idx in range(len(args.parameters)):
624
            arg = args.parameters[idx]
625
            if '=' in arg:
626
                k, v = arg.split('=', 1)
627
628
                # Attribute for files are prefixed with "@"
629
                if k.startswith('@'):
630
                    k = k[1:]
631
                    is_file = True
632
                else:
633
                    is_file = False
634
635
                try:
636
                    if is_file:
637
                        # Files are handled a bit differently since we ship the content
638
                        # over the wire
639
                        file_path = os.path.normpath(pjoin(os.getcwd(), v))
640
                        file_name = os.path.basename(file_path)
641
                        content = read_file(file_path=file_path)
642
643
                        if action_ref_or_id == 'core.http':
644
                            # Special case for http runner
645
                            result['_file_name'] = file_name
646
                            result['file_content'] = content
647
                        else:
648
                            result[k] = content
649
                    else:
650
                        # This permits multiple declarations of argument only in the array type.
651
                        if get_param_type(k) == 'array' and k in result:
652
                            result[k] += normalize(k, v)
653
                        else:
654
                            result[k] = normalize(k, v)
655
656
                except Exception as e:
657
                    # TODO: Move transformers in a separate module and handle
658
                    # exceptions there
659
                    if 'malformed string' in str(e):
660
                        message = ('Invalid value for boolean parameter. '
661
                                   'Valid values are: true, false')
662
                        raise ValueError(message)
663
                    else:
664
                        raise e
665
            else:
666
                result['cmd'] = ' '.join(args.parameters[idx:])
667
                break
668
669
        # Special case for http runner
670
        if 'file_content' in result:
671
            if 'method' not in result:
672
                # Default to POST if a method is not provided
673
                result['method'] = 'POST'
674
675
            if 'file_name' not in result:
676
                # File name not provided, use default file name
677
                result['file_name'] = result['_file_name']
678
679
            del result['_file_name']
680
681
        if args.inherit_env:
682
            result['env'] = self._get_inherited_env_vars()
683
684
        return result
685
686
    @add_auth_token_to_kwargs_from_cli
687
    def _print_help(self, args, **kwargs):
688
        # Print appropriate help message if the help option is given.
689
        action_mgr = self.app.client.managers['Action']
690
        action_exec_mgr = self.app.client.managers['LiveAction']
691
692
        if args.help:
693
            action_ref_or_id = getattr(args, 'ref_or_id', None)
694
            action_exec_id = getattr(args, 'id', None)
695
696
            if action_exec_id and not action_ref_or_id:
697
                action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs)
698
                args.ref_or_id = action_exec.action
699
700
            if action_ref_or_id:
701
                try:
702
                    action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs)
703
                    if not action:
704
                        raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id)
705
                    runner_mgr = self.app.client.managers['RunnerType']
706
                    runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
707
                    parameters, required, optional, _ = self._get_params_types(runner,
708
                                                                               action)
709
                    print('')
710
                    print(textwrap.fill(action.description))
711
                    print('')
712
                    if required:
713
                        required = self._sort_parameters(parameters=parameters,
714
                                                         names=required)
715
716
                        print('Required Parameters:')
717
                        [self._print_param(name, parameters.get(name))
718
                            for name in required]
719
                    if optional:
720
                        optional = self._sort_parameters(parameters=parameters,
721
                                                         names=optional)
722
723
                        print('Optional Parameters:')
724
                        [self._print_param(name, parameters.get(name))
725
                            for name in optional]
726
                except resource.ResourceNotFoundError:
727
                    print(('Action "%s" is not found. ' % args.ref_or_id) +
728
                          'Do "st2 action list" to see list of available actions.')
729
                except Exception as e:
730
                    print('ERROR: Unable to print help for action "%s". %s' %
731
                          (args.ref_or_id, e))
732
            else:
733
                self.parser.print_help()
734
            return True
735
        return False
736
737
    @staticmethod
738
    def _print_param(name, schema):
739
        if not schema:
740
            raise ValueError('Missing schema for parameter "%s"' % (name))
741
742
        wrapper = textwrap.TextWrapper(width=78)
743
        wrapper.initial_indent = ' ' * 4
744
        wrapper.subsequent_indent = wrapper.initial_indent
745
        print(wrapper.fill(name))
746
        wrapper.initial_indent = ' ' * 8
747
        wrapper.subsequent_indent = wrapper.initial_indent
748
        if 'description' in schema and schema['description']:
749
            print(wrapper.fill(schema['description']))
750
        if 'type' in schema and schema['type']:
751
            print(wrapper.fill('Type: %s' % schema['type']))
752
        if 'enum' in schema and schema['enum']:
753
            print(wrapper.fill('Enum: %s' % ', '.join(schema['enum'])))
754
        if 'default' in schema and schema['default'] is not None:
755
            print(wrapper.fill('Default: %s' % schema['default']))
756
        print('')
757
758
    @staticmethod
759
    def _get_params_types(runner, action):
760
        runner_params = runner.runner_parameters
761
        action_params = action.parameters
762
        parameters = copy.copy(runner_params)
763
        parameters.update(copy.copy(action_params))
764
        required = set([k for k, v in six.iteritems(parameters) if v.get('required')])
765
766
        def is_immutable(runner_param_meta, action_param_meta):
767
            # If runner sets a param as immutable, action cannot override that.
768
            if runner_param_meta.get('immutable', False):
769
                return True
770
            else:
771
                return action_param_meta.get('immutable', False)
772
773
        immutable = set()
774
        for param in parameters.keys():
775
            if is_immutable(runner_params.get(param, {}),
776
                            action_params.get(param, {})):
777
                immutable.add(param)
778
779
        required = required - immutable
780
        optional = set(parameters.keys()) - required - immutable
781
782
        return parameters, required, optional, immutable
783
784
    def _format_child_instances(self, children, parent_id):
785
        '''
786
        The goal of this method is to add an indent at every level. This way the
787
        WF is represented as a tree structure while in a list. For the right visuals
788
        representation the list must be a DF traversal else the idents will end up
789
        looking strange.
790
        '''
791
        # apply basic WF formating first.
792
        children = format_wf_instances(children)
793
        # setup a depth lookup table
794
        depth = {parent_id: 0}
795
        result = []
796
        # main loop that indents each entry correctly
797
        for child in children:
798
            # make sure child.parent is in depth and while at it compute the
799
            # right depth for indentation purposes.
800
            if child.parent not in depth:
801
                parent = None
802
                for instance in children:
803
                    if WF_PREFIX in instance.id:
804
                        instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):]
805
                    else:
806
                        instance_id = instance.id
807
                    if instance_id == child.parent:
808
                        parent = instance
809
                if parent and parent.parent and parent.parent in depth:
810
                    depth[child.parent] = depth[parent.parent] + 1
811
                else:
812
                    depth[child.parent] = 0
813
            # now ident for the right visuals
814
            child.id = INDENT_CHAR * depth[child.parent] + child.id
815
            result.append(self._format_for_common_representation(child))
816
        return result
817
818
    def _format_for_common_representation(self, task):
819
        '''
820
        Formats a task for common representation between mistral and action-chain.
821
        '''
822
        # This really needs to be better handled on the back-end but that would be a bigger
823
        # change so handling in cli.
824
        context = getattr(task, 'context', None)
825
        if context and 'chain' in context:
826
            task_name_key = 'context.chain.name'
827
        elif context and 'mistral' in context:
828
            task_name_key = 'context.mistral.task_name'
829
        # Use LiveAction as the object so that the formatter lookup does not change.
830
        # AKA HACK!
831
        return models.action.LiveAction(**{
832
            'id': task.id,
833
            'status': task.status,
834
            'task': jsutil.get_value(vars(task), task_name_key),
835
            'action': task.action.get('ref', None),
836
            'start_timestamp': task.start_timestamp,
837
            'end_timestamp': getattr(task, 'end_timestamp', None)
838
        })
839
840
    def _sort_parameters(self, parameters, names):
841
        """
842
        Sort a provided list of action parameters.
843
844
        :type parameters: ``list``
845
        :type names: ``list`` or ``set``
846
        """
847
        sorted_parameters = sorted(names, key=lambda name:
848
                                   self._get_parameter_sort_value(
849
                                       parameters=parameters,
850
                                       name=name))
851
852
        return sorted_parameters
853
854
    def _get_parameter_sort_value(self, parameters, name):
855
        """
856
        Return a value which determines sort order for a particular parameter.
857
858
        By default, parameters are sorted using "position" parameter attribute.
859
        If this attribute is not available, parameter is sorted based on the
860
        name.
861
        """
862
        parameter = parameters.get(name, None)
863
864
        if not parameter:
865
            return None
866
867
        sort_value = parameter.get('position', name)
868
        return sort_value
869
870
    def _get_inherited_env_vars(self):
871
        env_vars = os.environ.copy()
872
873
        for var_name in ENV_VARS_BLACKLIST:
874
            if var_name.lower() in env_vars:
875
                del env_vars[var_name.lower()]
876
            if var_name.upper() in env_vars:
877
                del env_vars[var_name.upper()]
878
879
        return env_vars
880
881
882
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
883
    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...
884
885
        super(ActionRunCommand, self).__init__(
886
            resource, kwargs.pop('name', 'execute'),
887
            'A command to invoke an action manually.',
888
            *args, **kwargs)
889
890
        self.parser.add_argument('ref_or_id', nargs='?',
891
                                 metavar='ref-or-id',
892
                                 help='Action reference (pack.action_name) ' +
893
                                 'or ID of the action.')
894
        self.parser.add_argument('parameters', nargs='*',
895
                                 help='List of keyword args, positional args, '
896
                                      'and optional args for the action.')
897
898
        self.parser.add_argument('-h', '--help',
899
                                 action='store_true', dest='help',
900
                                 help='Print usage for the given action.')
901
902
        self._add_common_options()
903
904
        if self.name in ['run', 'execute']:
905
            self.parser.add_argument('--trace-tag', '--trace_tag',
906
                                     help='A trace tag string to track execution later.',
907
                                     dest='trace_tag', required=False)
908
            self.parser.add_argument('--trace-id',
909
                                     help='Existing trace id for this execution.',
910
                                     dest='trace_id', required=False)
911
            self.parser.add_argument('-a', '--async',
912
                                     action='store_true', dest='async',
913
                                     help='Do not wait for action to finish.')
914
            self.parser.add_argument('-e', '--inherit-env',
915
                                     action='store_true', dest='inherit_env',
916
                                     help='Pass all the environment variables '
917
                                          'which are accessible to the CLI as "env" '
918
                                          'parameter to the action. Note: Only works '
919
                                          'with python, local and remote runners.')
920
            self.parser.add_argument('-u', '--user', type=str, default=None,
921
                                           help='User under which to run the action (admins only).')
922
923
        if self.name == 'run':
924
            self.parser.set_defaults(async=False)
925
        else:
926
            self.parser.set_defaults(async=True)
927
928
    @add_auth_token_to_kwargs_from_cli
929
    def run(self, args, **kwargs):
930
        if not args.ref_or_id:
931
            self.parser.error('Missing action reference or id')
932
933
        action = self.get_resource(args.ref_or_id, **kwargs)
934
        if not action:
935
            raise resource.ResourceNotFoundError('Action "%s" cannot be found.'
936
                                                 % (args.ref_or_id))
937
938
        runner_mgr = self.app.client.managers['RunnerType']
939
        runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
940
        if not runner:
941
            raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.'
942
                                                 % (action.runner_type, action.name))
943
944
        action_ref = '.'.join([action.pack, action.name])
945
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
946
                                                                  args=args)
947
948
        execution = models.LiveAction()
949
        execution.action = action_ref
950
        execution.parameters = action_parameters
951
        execution.user = args.user
952
953
        if not args.trace_id and args.trace_tag:
954
            execution.context = {'trace_context': {'trace_tag': args.trace_tag}}
955
956
        if args.trace_id:
957
            execution.context = {'trace_context': {'id_': args.trace_id}}
958
959
        action_exec_mgr = self.app.client.managers['LiveAction']
960
961
        execution = action_exec_mgr.create(execution, **kwargs)
962
        execution = self._get_execution_result(execution=execution,
963
                                               action_exec_mgr=action_exec_mgr,
964
                                               args=args, **kwargs)
965
        return execution
966
967
968
class ActionExecutionBranch(resource.ResourceBranch):
969
970
    def __init__(self, description, app, subparsers, parent_parser=None):
971
        super(ActionExecutionBranch, self).__init__(
972
            models.LiveAction, description, app, subparsers,
973
            parent_parser=parent_parser, read_only=True,
974
            commands={'list': ActionExecutionListCommand,
975
                      'get': ActionExecutionGetCommand})
976
977
        # Register extended commands
978
        self.commands['re-run'] = ActionExecutionReRunCommand(
979
            self.resource, self.app, self.subparsers, add_help=False)
980
        self.commands['cancel'] = ActionExecutionCancelCommand(
981
            self.resource, self.app, self.subparsers, add_help=False)
982
        self.commands['pause'] = ActionExecutionPauseCommand(
983
            self.resource, self.app, self.subparsers, add_help=False)
984
        self.commands['resume'] = ActionExecutionResumeCommand(
985
            self.resource, self.app, self.subparsers, add_help=False)
986
987
988
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled')
989
990
991
class ActionExecutionReadCommand(resource.ResourceCommand):
992
    """
993
    Base class for read / view commands (list and get).
994
    """
995
996
    @classmethod
997
    def _get_exclude_attributes(cls, args):
998
        """
999
        Retrieve a list of exclude attributes for particular command line arguments.
1000
        """
1001
        exclude_attributes = []
1002
1003
        result_included = False
1004
        trigger_instance_included = False
1005
1006
        for attr in args.attr:
1007
            # Note: We perform startswith check so we correctly detected child attribute properties
1008
            # (e.g. result, result.stdout, result.stderr, etc.)
1009
            if attr.startswith('result'):
1010
                result_included = True
1011
1012
            if attr.startswith('trigger_instance'):
1013
                trigger_instance_included = True
1014
1015
        if not result_included:
1016
            exclude_attributes.append('result')
1017
        if not trigger_instance_included:
1018
            exclude_attributes.append('trigger_instance')
1019
1020
        return exclude_attributes
1021
1022
1023
class ActionExecutionListCommand(ActionExecutionReadCommand):
1024
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
1025
                          'end_timestamp']
1026
    attribute_transform_functions = {
1027
        'start_timestamp': format_isodate_for_user_timezone,
1028
        'end_timestamp': format_isodate_for_user_timezone,
1029
        'parameters': format_parameters,
1030
        'status': format_status
1031
    }
1032
1033
    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...
1034
        super(ActionExecutionListCommand, self).__init__(
1035
            resource, 'list', 'Get the list of the 50 most recent %s.' %
1036
            resource.get_plural_display_name().lower(),
1037
            *args, **kwargs)
1038
1039
        self.default_limit = 50
1040
        self.resource_name = resource.get_plural_display_name().lower()
1041
        self.group = self.parser.add_argument_group()
1042
        self.parser.add_argument('-n', '--last', type=int, dest='last',
1043
                                 default=self.default_limit,
1044
                                 help=('List N most recent %s.' % self.resource_name))
1045
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
1046
                                 default='descending',
1047
                                 help=('Sort %s by start timestamp, '
1048
                                       'asc|ascending (earliest first) '
1049
                                       'or desc|descending (latest first)' % self.resource_name))
1050
1051
        # Filter options
1052
        self.group.add_argument('--action', help='Action reference to filter the list.')
1053
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
1054
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
1055
                                                  '\'%s\' or \'%s\''
1056
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
1057
        self.group.add_argument('--trigger_instance',
1058
                                help='Trigger instance id to filter the list.')
1059
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
1060
                                 default=None,
1061
                                 help=('Only return executions with timestamp '
1062
                                       'greater than the one provided. '
1063
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1064
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
1065
                                 default=None,
1066
                                 help=('Only return executions with timestamp '
1067
                                       'lower than the one provided. '
1068
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1069
        self.parser.add_argument('-l', '--showall', action='store_true',
1070
                                 help='')
1071
1072
        # Display options
1073
        self.parser.add_argument('-a', '--attr', nargs='+',
1074
                                 default=self.display_attributes,
1075
                                 help=('List of attributes to include in the '
1076
                                       'output. "all" will return all '
1077
                                       'attributes.'))
1078
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
1079
                                 default=None,
1080
                                 help=('Set the width of columns in output.'))
1081
1082
    @add_auth_token_to_kwargs_from_cli
1083
    def run(self, args, **kwargs):
1084
        # Filtering options
1085
        if args.action:
1086
            kwargs['action'] = args.action
1087
        if args.status:
1088
            kwargs['status'] = args.status
1089
        if args.trigger_instance:
1090
            kwargs['trigger_instance'] = args.trigger_instance
1091
        if not args.showall:
1092
            # null is the magic string that translates to does not exist.
1093
            kwargs['parent'] = 'null'
1094
        if args.timestamp_gt:
1095
            kwargs['timestamp_gt'] = args.timestamp_gt
1096
        if args.timestamp_lt:
1097
            kwargs['timestamp_lt'] = args.timestamp_lt
1098
        if args.sort_order:
1099
            if args.sort_order in ['asc', 'ascending']:
1100
                kwargs['sort_asc'] = True
1101
            elif args.sort_order in ['desc', 'descending']:
1102
                kwargs['sort_desc'] = True
1103
1104
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
1105
        # since they are not displayed nor used which speeds the common operation substantially.
1106
        exclude_attributes = self._get_exclude_attributes(args=args)
1107
        exclude_attributes = ','.join(exclude_attributes)
1108
        kwargs['exclude_attributes'] = exclude_attributes
1109
1110
        return self.manager.query_with_count(limit=args.last, **kwargs)
1111
1112
    def run_and_print(self, args, **kwargs):
1113
1114
        result, count = self.run(args, **kwargs)
1115
        instances = format_wf_instances(result)
1116
1117
        if args.json or args.yaml:
1118
            self.print_output(reversed(instances), table.MultiColumnTable,
1119
                              attributes=args.attr, widths=args.width,
1120
                              json=args.json,
1121
                              yaml=args.yaml,
1122
                              attribute_transform_functions=self.attribute_transform_functions)
1123
1124
        else:
1125
            # Include elapsed time for running executions
1126
            instances = format_execution_statuses(instances)
1127
            self.print_output(reversed(instances), table.MultiColumnTable,
1128
                              attributes=args.attr, widths=args.width,
1129
                              attribute_transform_functions=self.attribute_transform_functions)
1130
1131
            if args.last and count and count > args.last:
1132
                table.SingleRowTable.note_box(self.resource_name, args.last)
1133
1134
1135
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1136
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1137
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1138
1139
    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...
1140
        super(ActionExecutionGetCommand, self).__init__(
1141
            resource, 'get',
1142
            'Get individual %s.' % resource.get_display_name().lower(),
1143
            *args, **kwargs)
1144
1145
        self.parser.add_argument('id',
1146
                                 help=('ID of the %s.' %
1147
                                       resource.get_display_name().lower()))
1148
1149
        self._add_common_options()
1150
1151
    @add_auth_token_to_kwargs_from_cli
1152
    def run(self, args, **kwargs):
1153
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1154
        # requested by user either via "--attr" flag or by default.
1155
        exclude_attributes = self._get_exclude_attributes(args=args)
1156
        exclude_attributes = ','.join(exclude_attributes)
1157
1158
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1159
1160
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1161
        return execution
1162
1163
    @add_auth_token_to_kwargs_from_cli
1164
    def run_and_print(self, args, **kwargs):
1165
        try:
1166
            execution = self.run(args, **kwargs)
1167
1168
            if not args.json and not args.yaml:
1169
                # Include elapsed time for running executions
1170
                execution = format_execution_status(execution)
1171
        except resource.ResourceNotFoundError:
1172
            self.print_not_found(args.id)
1173
            raise OperationFailureException('Execution %s not found.' % (args.id))
1174
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1175
1176
1177
class ActionExecutionCancelCommand(resource.ResourceCommand):
1178
1179
    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...
1180
        super(ActionExecutionCancelCommand, self).__init__(
1181
            resource, 'cancel', 'Cancel %s.' %
1182
            resource.get_plural_display_name().lower(),
1183
            *args, **kwargs)
1184
1185
        self.parser.add_argument('ids',
1186
                                 nargs='+',
1187
                                 help=('IDs of the %ss to cancel.' %
1188
                                       resource.get_display_name().lower()))
1189
1190
    def run(self, args, **kwargs):
1191
        responses = []
1192
        for execution_id in args.ids:
1193
            response = self.manager.delete_by_id(execution_id)
1194
            responses.append([execution_id, response])
1195
1196
        return responses
1197
1198
    @add_auth_token_to_kwargs_from_cli
1199
    def run_and_print(self, args, **kwargs):
1200
        responses = self.run(args, **kwargs)
1201
1202
        for execution_id, response in responses:
1203
            self._print_result(execution_id=execution_id, response=response)
1204
1205
    def _print_result(self, execution_id, response):
1206
        if response and 'faultstring' in response:
1207
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1208
                                   (self.resource.get_display_name().lower(), execution_id))
1209
1210
        elif response:
1211
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1212
                                                   execution_id)
1213
        else:
1214
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1215
                                                        execution_id)
1216
        print(message)
1217
1218
1219
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1220
    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...
1221
1222
        super(ActionExecutionReRunCommand, self).__init__(
1223
            resource, kwargs.pop('name', 're-run'),
1224
            'A command to re-run a particular action.',
1225
            *args, **kwargs)
1226
1227
        self.parser.add_argument('id', nargs='?',
1228
                                 metavar='id',
1229
                                 help='ID of action execution to re-run ')
1230
        self.parser.add_argument('parameters', nargs='*',
1231
                                 help='List of keyword args, positional args, '
1232
                                      'and optional args for the action.')
1233
        self.parser.add_argument('--tasks', nargs='*',
1234
                                 help='Name of the workflow tasks to re-run.')
1235
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1236
                                 help='Name of the with-items tasks to not reset. This only '
1237
                                      'applies to Mistral workflows. By default, all iterations '
1238
                                      'for with-items tasks is rerun. If no reset, only failed '
1239
                                      ' iterations are rerun.')
1240
        self.parser.add_argument('-a', '--async',
1241
                                 action='store_true', dest='async',
1242
                                 help='Do not wait for action to finish.')
1243
        self.parser.add_argument('-e', '--inherit-env',
1244
                                 action='store_true', dest='inherit_env',
1245
                                 help='Pass all the environment variables '
1246
                                      'which are accessible to the CLI as "env" '
1247
                                      'parameter to the action. Note: Only works '
1248
                                      'with python, local and remote runners.')
1249
        self.parser.add_argument('-h', '--help',
1250
                                 action='store_true', dest='help',
1251
                                 help='Print usage for the given action.')
1252
1253
        self._add_common_options()
1254
1255
    @add_auth_token_to_kwargs_from_cli
1256
    def run(self, args, **kwargs):
1257
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1258
1259
        if not existing_execution:
1260
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1261
                                                 (args.id))
1262
1263
        action_mgr = self.app.client.managers['Action']
1264
        runner_mgr = self.app.client.managers['RunnerType']
1265
        action_exec_mgr = self.app.client.managers['LiveAction']
1266
1267
        action_ref = existing_execution.action['ref']
1268
        action = action_mgr.get_by_ref_or_id(action_ref)
1269
        runner = runner_mgr.get_by_name(action.runner_type)
1270
1271
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1272
                                                                  args=args)
1273
1274
        execution = action_exec_mgr.re_run(execution_id=args.id,
1275
                                           parameters=action_parameters,
1276
                                           tasks=args.tasks,
1277
                                           no_reset=args.no_reset,
1278
                                           **kwargs)
1279
1280
        execution = self._get_execution_result(execution=execution,
1281
                                               action_exec_mgr=action_exec_mgr,
1282
                                               args=args, **kwargs)
1283
1284
        return execution
1285
1286
1287
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1288
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1289
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1290
1291
    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...
1292
        super(ActionExecutionPauseCommand, self).__init__(
1293
            resource, 'pause', 'Pause %s (workflow executions only).' %
1294
            resource.get_plural_display_name().lower(),
1295
            *args, **kwargs)
1296
1297
        self.parser.add_argument('id', nargs='?',
1298
                                 metavar='id',
1299
                                 help='ID of action execution to pause.')
1300
1301
        self._add_common_options()
1302
1303
    @add_auth_token_to_kwargs_from_cli
1304
    def run(self, args, **kwargs):
1305
        return self.manager.pause(args.id)
1306
1307
    @add_auth_token_to_kwargs_from_cli
1308
    def run_and_print(self, args, **kwargs):
1309
        try:
1310
            execution = self.run(args, **kwargs)
1311
1312
            if not args.json and not args.yaml:
1313
                # Include elapsed time for running executions
1314
                execution = format_execution_status(execution)
1315
        except resource.ResourceNotFoundError:
1316
            self.print_not_found(args.id)
1317
            raise OperationFailureException('Execution %s not found.' % (args.id))
1318
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1319
1320
1321
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1322
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1323
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1324
1325
    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...
1326
        super(ActionExecutionResumeCommand, self).__init__(
1327
            resource, 'resume', 'Resume %s (workflow executions only).' %
1328
            resource.get_plural_display_name().lower(),
1329
            *args, **kwargs)
1330
1331
        self.parser.add_argument('id', nargs='?',
1332
                                 metavar='id',
1333
                                 help='ID of action execution to resume.')
1334
1335
        self._add_common_options()
1336
1337
    @add_auth_token_to_kwargs_from_cli
1338
    def run(self, args, **kwargs):
1339
        return self.manager.resume(args.id)
1340
1341
    @add_auth_token_to_kwargs_from_cli
1342
    def run_and_print(self, args, **kwargs):
1343
        try:
1344
            execution = self.run(args, **kwargs)
1345
1346
            if not args.json and not args.yaml:
1347
                # Include elapsed time for running executions
1348
                execution = format_execution_status(execution)
1349
        except resource.ResourceNotFoundError:
1350
            self.print_not_found(args.id)
1351
            raise OperationFailureException('Execution %s not found.' % (args.id))
1352
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1353