Passed
Pull Request — master (#3507)
by W
05:41
created

ActionExecutionPauseCommand   A

Complexity

Total Complexity 6

Size/Duplication

Total Lines 32
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 32
rs 10
c 1
b 0
f 0
wmc 6

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 11 1
A run_and_print() 0 12 4
A run() 0 3 1
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 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):
527
            # Sometimes an array parameter only has a single element:
528
            #
529
            #     i.e. "st2 run foopack.fooaction arrayparam=51"
530
            #
531
            # Normally, json.loads would throw an exception, and the split method
532
            # would be used. However, since this is an int, not only would
533
            # splitting not work, but json.loads actually treats this as valid JSON,
534
            # but as an int, not an array. This causes a mismatch when the API is called.
535
            #
536
            # We want to try to handle this first, so it doesn't get accidentally
537
            # sent to the API as an int, instead of an array of single-element int.
538
            try:
539
                # Force this to be a list containing the single int, then
540
                # cast the whole thing to string so json.loads can handle it
541
                value = str([int(value)])
542
            except ValueError:
543
                # Original value wasn't an int, so just let it continue
544
                pass
545
546
            # At this point, the input is either a a "json.loads"-able construct
547
            # like [1, 2, 3], or even [1], or it is a comma-separated list,
548
            # Try both, in that order.
549
            try:
550
                result = json.loads(value)
551
            except ValueError:
552
                result = [v.strip() for v in value.split(',')]
553
            return result
554
555
        transformer = {
556
            'array': transform_array,
557
            'boolean': (lambda x: ast.literal_eval(x.capitalize())),
558
            'integer': int,
559
            'number': float,
560
            'object': transform_object,
561
            'string': str
562
        }
563
564
        def normalize(name, value):
565
            """ The desired type is contained in the action meta-data, so we can look that up
566
                and call the desired "caster" function listed in the "transformer" dict
567
            """
568
569
            # Users can also specify type for each array parameter inside an action metadata
570
            # (items: type: int for example) and this information is available here so we could
571
            # also leverage that to cast each array item to the correct type.
572
573
            if name in runner.runner_parameters:
574
                param = runner.runner_parameters[name]
575
                if 'type' in param and param['type'] in transformer:
576
                    return transformer[param['type']](value)
577
578
            if name in action.parameters:
579
                param = action.parameters[name]
580
                if 'type' in param and param['type'] in transformer:
581
                    return transformer[param['type']](value)
582
            return value
583
584
        result = {}
585
586
        if not args.parameters:
587
            return result
588
589
        for idx in range(len(args.parameters)):
590
            arg = args.parameters[idx]
591
            if '=' in arg:
592
                k, v = arg.split('=', 1)
593
594
                # Attribute for files are prefixed with "@"
595
                if k.startswith('@'):
596
                    k = k[1:]
597
                    is_file = True
598
                else:
599
                    is_file = False
600
601
                try:
602
                    if is_file:
603
                        # Files are handled a bit differently since we ship the content
604
                        # over the wire
605
                        file_path = os.path.normpath(pjoin(os.getcwd(), v))
606
                        file_name = os.path.basename(file_path)
607
                        content = read_file(file_path=file_path)
608
609
                        if action_ref_or_id == 'core.http':
610
                            # Special case for http runner
611
                            result['_file_name'] = file_name
612
                            result['file_content'] = content
613
                        else:
614
                            result[k] = content
615
                    else:
616
                        result[k] = normalize(k, v)
617
                except Exception as e:
618
                    # TODO: Move transformers in a separate module and handle
619
                    # exceptions there
620
                    if 'malformed string' in str(e):
621
                        message = ('Invalid value for boolean parameter. '
622
                                   'Valid values are: true, false')
623
                        raise ValueError(message)
624
                    else:
625
                        raise e
626
            else:
627
                result['cmd'] = ' '.join(args.parameters[idx:])
628
                break
629
630
        # Special case for http runner
631
        if 'file_content' in result:
632
            if 'method' not in result:
633
                # Default to POST if a method is not provided
634
                result['method'] = 'POST'
635
636
            if 'file_name' not in result:
637
                # File name not provided, use default file name
638
                result['file_name'] = result['_file_name']
639
640
            del result['_file_name']
641
642
        if args.inherit_env:
643
            result['env'] = self._get_inherited_env_vars()
644
645
        return result
646
647
    @add_auth_token_to_kwargs_from_cli
648
    def _print_help(self, args, **kwargs):
649
        # Print appropriate help message if the help option is given.
650
        action_mgr = self.app.client.managers['Action']
651
        action_exec_mgr = self.app.client.managers['LiveAction']
652
653
        if args.help:
654
            action_ref_or_id = getattr(args, 'ref_or_id', None)
655
            action_exec_id = getattr(args, 'id', None)
656
657
            if action_exec_id and not action_ref_or_id:
658
                action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs)
659
                args.ref_or_id = action_exec.action
660
661
            if action_ref_or_id:
662
                try:
663
                    action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs)
664
                    if not action:
665
                        raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id)
666
                    runner_mgr = self.app.client.managers['RunnerType']
667
                    runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
668
                    parameters, required, optional, _ = self._get_params_types(runner,
669
                                                                               action)
670
                    print('')
671
                    print(textwrap.fill(action.description))
672
                    print('')
673
                    if required:
674
                        required = self._sort_parameters(parameters=parameters,
675
                                                         names=required)
676
677
                        print('Required Parameters:')
678
                        [self._print_param(name, parameters.get(name))
679
                            for name in required]
680
                    if optional:
681
                        optional = self._sort_parameters(parameters=parameters,
682
                                                         names=optional)
683
684
                        print('Optional Parameters:')
685
                        [self._print_param(name, parameters.get(name))
686
                            for name in optional]
687
                except resource.ResourceNotFoundError:
688
                    print(('Action "%s" is not found. ' % args.ref_or_id) +
689
                          'Do "st2 action list" to see list of available actions.')
690
                except Exception as e:
691
                    print('ERROR: Unable to print help for action "%s". %s' %
692
                          (args.ref_or_id, e))
693
            else:
694
                self.parser.print_help()
695
            return True
696
        return False
697
698
    @staticmethod
699
    def _print_param(name, schema):
700
        if not schema:
701
            raise ValueError('Missing schema for parameter "%s"' % (name))
702
703
        wrapper = textwrap.TextWrapper(width=78)
704
        wrapper.initial_indent = ' ' * 4
705
        wrapper.subsequent_indent = wrapper.initial_indent
706
        print(wrapper.fill(name))
707
        wrapper.initial_indent = ' ' * 8
708
        wrapper.subsequent_indent = wrapper.initial_indent
709
        if 'description' in schema and schema['description']:
710
            print(wrapper.fill(schema['description']))
711
        if 'type' in schema and schema['type']:
712
            print(wrapper.fill('Type: %s' % schema['type']))
713
        if 'enum' in schema and schema['enum']:
714
            print(wrapper.fill('Enum: %s' % ', '.join(schema['enum'])))
715
        if 'default' in schema and schema['default'] is not None:
716
            print(wrapper.fill('Default: %s' % schema['default']))
717
        print('')
718
719
    @staticmethod
720
    def _get_params_types(runner, action):
721
        runner_params = runner.runner_parameters
722
        action_params = action.parameters
723
        parameters = copy.copy(runner_params)
724
        parameters.update(copy.copy(action_params))
725
        required = set([k for k, v in six.iteritems(parameters) if v.get('required')])
726
727
        def is_immutable(runner_param_meta, action_param_meta):
728
            # If runner sets a param as immutable, action cannot override that.
729
            if runner_param_meta.get('immutable', False):
730
                return True
731
            else:
732
                return action_param_meta.get('immutable', False)
733
734
        immutable = set()
735
        for param in parameters.keys():
736
            if is_immutable(runner_params.get(param, {}),
737
                            action_params.get(param, {})):
738
                immutable.add(param)
739
740
        required = required - immutable
741
        optional = set(parameters.keys()) - required - immutable
742
743
        return parameters, required, optional, immutable
744
745
    def _format_child_instances(self, children, parent_id):
746
        '''
747
        The goal of this method is to add an indent at every level. This way the
748
        WF is represented as a tree structure while in a list. For the right visuals
749
        representation the list must be a DF traversal else the idents will end up
750
        looking strange.
751
        '''
752
        # apply basic WF formating first.
753
        children = format_wf_instances(children)
754
        # setup a depth lookup table
755
        depth = {parent_id: 0}
756
        result = []
757
        # main loop that indents each entry correctly
758
        for child in children:
759
            # make sure child.parent is in depth and while at it compute the
760
            # right depth for indentation purposes.
761
            if child.parent not in depth:
762
                parent = None
763
                for instance in children:
764
                    if WF_PREFIX in instance.id:
765
                        instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):]
766
                    else:
767
                        instance_id = instance.id
768
                    if instance_id == child.parent:
769
                        parent = instance
770
                if parent and parent.parent and parent.parent in depth:
771
                    depth[child.parent] = depth[parent.parent] + 1
772
                else:
773
                    depth[child.parent] = 0
774
            # now ident for the right visuals
775
            child.id = INDENT_CHAR * depth[child.parent] + child.id
776
            result.append(self._format_for_common_representation(child))
777
        return result
778
779
    def _format_for_common_representation(self, task):
780
        '''
781
        Formats a task for common representation between mistral and action-chain.
782
        '''
783
        # This really needs to be better handled on the back-end but that would be a bigger
784
        # change so handling in cli.
785
        context = getattr(task, 'context', None)
786
        if context and 'chain' in context:
787
            task_name_key = 'context.chain.name'
788
        elif context and 'mistral' in context:
789
            task_name_key = 'context.mistral.task_name'
790
        # Use LiveAction as the object so that the formatter lookup does not change.
791
        # AKA HACK!
792
        return models.action.LiveAction(**{
793
            'id': task.id,
794
            'status': task.status,
795
            'task': jsutil.get_value(vars(task), task_name_key),
796
            'action': task.action.get('ref', None),
797
            'start_timestamp': task.start_timestamp,
798
            'end_timestamp': getattr(task, 'end_timestamp', None)
799
        })
800
801
    def _sort_parameters(self, parameters, names):
802
        """
803
        Sort a provided list of action parameters.
804
805
        :type parameters: ``list``
806
        :type names: ``list`` or ``set``
807
        """
808
        sorted_parameters = sorted(names, key=lambda name:
809
                                   self._get_parameter_sort_value(
810
                                       parameters=parameters,
811
                                       name=name))
812
813
        return sorted_parameters
814
815
    def _get_parameter_sort_value(self, parameters, name):
816
        """
817
        Return a value which determines sort order for a particular parameter.
818
819
        By default, parameters are sorted using "position" parameter attribute.
820
        If this attribute is not available, parameter is sorted based on the
821
        name.
822
        """
823
        parameter = parameters.get(name, None)
824
825
        if not parameter:
826
            return None
827
828
        sort_value = parameter.get('position', name)
829
        return sort_value
830
831
    def _get_inherited_env_vars(self):
832
        env_vars = os.environ.copy()
833
834
        for var_name in ENV_VARS_BLACKLIST:
835
            if var_name.lower() in env_vars:
836
                del env_vars[var_name.lower()]
837
            if var_name.upper() in env_vars:
838
                del env_vars[var_name.upper()]
839
840
        return env_vars
841
842
843
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
844
    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...
845
846
        super(ActionRunCommand, self).__init__(
847
            resource, kwargs.pop('name', 'execute'),
848
            'A command to invoke an action manually.',
849
            *args, **kwargs)
850
851
        self.parser.add_argument('ref_or_id', nargs='?',
852
                                 metavar='ref-or-id',
853
                                 help='Action reference (pack.action_name) ' +
854
                                 'or ID of the action.')
855
        self.parser.add_argument('parameters', nargs='*',
856
                                 help='List of keyword args, positional args, '
857
                                      'and optional args for the action.')
858
859
        self.parser.add_argument('-h', '--help',
860
                                 action='store_true', dest='help',
861
                                 help='Print usage for the given action.')
862
863
        self._add_common_options()
864
865
        if self.name in ['run', 'execute']:
866
            self.parser.add_argument('--trace-tag', '--trace_tag',
867
                                     help='A trace tag string to track execution later.',
868
                                     dest='trace_tag', required=False)
869
            self.parser.add_argument('--trace-id',
870
                                     help='Existing trace id for this execution.',
871
                                     dest='trace_id', required=False)
872
            self.parser.add_argument('-a', '--async',
873
                                     action='store_true', dest='async',
874
                                     help='Do not wait for action to finish.')
875
            self.parser.add_argument('-e', '--inherit-env',
876
                                     action='store_true', dest='inherit_env',
877
                                     help='Pass all the environment variables '
878
                                          'which are accessible to the CLI as "env" '
879
                                          'parameter to the action. Note: Only works '
880
                                          'with python, local and remote runners.')
881
            self.parser.add_argument('-u', '--user', type=str, default=None,
882
                                           help='User under which to run the action (admins only).')
883
884
        if self.name == 'run':
885
            self.parser.set_defaults(async=False)
886
        else:
887
            self.parser.set_defaults(async=True)
888
889
    @add_auth_token_to_kwargs_from_cli
890
    def run(self, args, **kwargs):
891
        if not args.ref_or_id:
892
            self.parser.error('Missing action reference or id')
893
894
        action = self.get_resource(args.ref_or_id, **kwargs)
895
        if not action:
896
            raise resource.ResourceNotFoundError('Action "%s" cannot be found.'
897
                                                 % (args.ref_or_id))
898
899
        runner_mgr = self.app.client.managers['RunnerType']
900
        runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
901
        if not runner:
902
            raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.'
903
                                                 % (action.runner_type, action.name))
904
905
        action_ref = '.'.join([action.pack, action.name])
906
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
907
                                                                  args=args)
908
909
        execution = models.LiveAction()
910
        execution.action = action_ref
911
        execution.parameters = action_parameters
912
        execution.user = args.user
913
914
        if not args.trace_id and args.trace_tag:
915
            execution.context = {'trace_context': {'trace_tag': args.trace_tag}}
916
917
        if args.trace_id:
918
            execution.context = {'trace_context': {'id_': args.trace_id}}
919
920
        action_exec_mgr = self.app.client.managers['LiveAction']
921
922
        execution = action_exec_mgr.create(execution, **kwargs)
923
        execution = self._get_execution_result(execution=execution,
924
                                               action_exec_mgr=action_exec_mgr,
925
                                               args=args, **kwargs)
926
        return execution
927
928
929
class ActionExecutionBranch(resource.ResourceBranch):
930
931
    def __init__(self, description, app, subparsers, parent_parser=None):
932
        super(ActionExecutionBranch, self).__init__(
933
            models.LiveAction, description, app, subparsers,
934
            parent_parser=parent_parser, read_only=True,
935
            commands={'list': ActionExecutionListCommand,
936
                      'get': ActionExecutionGetCommand})
937
938
        # Register extended commands
939
        self.commands['re-run'] = ActionExecutionReRunCommand(
940
            self.resource, self.app, self.subparsers, add_help=False)
941
        self.commands['cancel'] = ActionExecutionCancelCommand(
942
            self.resource, self.app, self.subparsers, add_help=False)
943
        self.commands['pause'] = ActionExecutionPauseCommand(
944
            self.resource, self.app, self.subparsers, add_help=False)
945
        self.commands['resume'] = ActionExecutionResumeCommand(
946
            self.resource, self.app, self.subparsers, add_help=False)
947
948
949
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled')
950
951
952
class ActionExecutionReadCommand(resource.ResourceCommand):
953
    """
954
    Base class for read / view commands (list and get).
955
    """
956
957
    @classmethod
958
    def _get_exclude_attributes(cls, args):
959
        """
960
        Retrieve a list of exclude attributes for particular command line arguments.
961
        """
962
        exclude_attributes = []
963
964
        result_included = False
965
        trigger_instance_included = False
966
967
        for attr in args.attr:
968
            # Note: We perform startswith check so we correctly detected child attribute properties
969
            # (e.g. result, result.stdout, result.stderr, etc.)
970
            if attr.startswith('result'):
971
                result_included = True
972
973
            if attr.startswith('trigger_instance'):
974
                trigger_instance_included = True
975
976
        if not result_included:
977
            exclude_attributes.append('result')
978
        if not trigger_instance_included:
979
            exclude_attributes.append('trigger_instance')
980
981
        return exclude_attributes
982
983
984
class ActionExecutionListCommand(ActionExecutionReadCommand):
985
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
986
                          'end_timestamp']
987
    attribute_transform_functions = {
988
        'start_timestamp': format_isodate_for_user_timezone,
989
        'end_timestamp': format_isodate_for_user_timezone,
990
        'parameters': format_parameters,
991
        'status': format_status
992
    }
993
994
    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...
995
        super(ActionExecutionListCommand, self).__init__(
996
            resource, 'list', 'Get the list of the 50 most recent %s.' %
997
            resource.get_plural_display_name().lower(),
998
            *args, **kwargs)
999
1000
        self.default_limit = 50
1001
        self.resource_name = resource.get_plural_display_name().lower()
1002
        self.group = self.parser.add_argument_group()
1003
        self.parser.add_argument('-n', '--last', type=int, dest='last',
1004
                                 default=self.default_limit,
1005
                                 help=('List N most recent %s.' % self.resource_name))
1006
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
1007
                                 default='descending',
1008
                                 help=('Sort %s by start timestamp, '
1009
                                       'asc|ascending (earliest first) '
1010
                                       'or desc|descending (latest first)' % self.resource_name))
1011
1012
        # Filter options
1013
        self.group.add_argument('--action', help='Action reference to filter the list.')
1014
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
1015
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
1016
                                                  '\'%s\' or \'%s\''
1017
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
1018
        self.group.add_argument('--trigger_instance',
1019
                                help='Trigger instance id to filter the list.')
1020
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
1021
                                 default=None,
1022
                                 help=('Only return executions with timestamp '
1023
                                       'greater than the one provided. '
1024
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1025
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
1026
                                 default=None,
1027
                                 help=('Only return executions with timestamp '
1028
                                       'lower than the one provided. '
1029
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1030
        self.parser.add_argument('-l', '--showall', action='store_true',
1031
                                 help='')
1032
1033
        # Display options
1034
        self.parser.add_argument('-a', '--attr', nargs='+',
1035
                                 default=self.display_attributes,
1036
                                 help=('List of attributes to include in the '
1037
                                       'output. "all" will return all '
1038
                                       'attributes.'))
1039
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
1040
                                 default=None,
1041
                                 help=('Set the width of columns in output.'))
1042
1043
    @add_auth_token_to_kwargs_from_cli
1044
    def run(self, args, **kwargs):
1045
        # Filtering options
1046
        if args.action:
1047
            kwargs['action'] = args.action
1048
        if args.status:
1049
            kwargs['status'] = args.status
1050
        if args.trigger_instance:
1051
            kwargs['trigger_instance'] = args.trigger_instance
1052
        if not args.showall:
1053
            # null is the magic string that translates to does not exist.
1054
            kwargs['parent'] = 'null'
1055
        if args.timestamp_gt:
1056
            kwargs['timestamp_gt'] = args.timestamp_gt
1057
        if args.timestamp_lt:
1058
            kwargs['timestamp_lt'] = args.timestamp_lt
1059
        if args.sort_order:
1060
            if args.sort_order in ['asc', 'ascending']:
1061
                kwargs['sort_asc'] = True
1062
            elif args.sort_order in ['desc', 'descending']:
1063
                kwargs['sort_desc'] = True
1064
1065
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
1066
        # since they are not displayed nor used which speeds the common operation substantially.
1067
        exclude_attributes = self._get_exclude_attributes(args=args)
1068
        exclude_attributes = ','.join(exclude_attributes)
1069
        kwargs['exclude_attributes'] = exclude_attributes
1070
1071
        return self.manager.query_with_count(limit=args.last, **kwargs)
1072
1073
    def run_and_print(self, args, **kwargs):
1074
1075
        result, count = self.run(args, **kwargs)
1076
        instances = format_wf_instances(result)
1077
1078
        if args.json or args.yaml:
1079
            self.print_output(reversed(instances), table.MultiColumnTable,
1080
                              attributes=args.attr, widths=args.width,
1081
                              json=args.json,
1082
                              yaml=args.yaml,
1083
                              attribute_transform_functions=self.attribute_transform_functions)
1084
1085
        else:
1086
            # Include elapsed time for running executions
1087
            instances = format_execution_statuses(instances)
1088
            self.print_output(reversed(instances), table.MultiColumnTable,
1089
                              attributes=args.attr, widths=args.width,
1090
                              attribute_transform_functions=self.attribute_transform_functions)
1091
1092
            if args.last and count and count > args.last:
1093
                table.SingleRowTable.note_box(self.resource_name, args.last)
1094
1095
1096
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1097
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1098
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1099
1100
    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...
1101
        super(ActionExecutionGetCommand, self).__init__(
1102
            resource, 'get',
1103
            'Get individual %s.' % resource.get_display_name().lower(),
1104
            *args, **kwargs)
1105
1106
        self.parser.add_argument('id',
1107
                                 help=('ID of the %s.' %
1108
                                       resource.get_display_name().lower()))
1109
1110
        self._add_common_options()
1111
1112
    @add_auth_token_to_kwargs_from_cli
1113
    def run(self, args, **kwargs):
1114
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1115
        # requested by user either via "--attr" flag or by default.
1116
        exclude_attributes = self._get_exclude_attributes(args=args)
1117
        exclude_attributes = ','.join(exclude_attributes)
1118
1119
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1120
1121
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1122
        return execution
1123
1124
    @add_auth_token_to_kwargs_from_cli
1125
    def run_and_print(self, args, **kwargs):
1126
        try:
1127
            execution = self.run(args, **kwargs)
1128
1129
            if not args.json and not args.yaml:
1130
                # Include elapsed time for running executions
1131
                execution = format_execution_status(execution)
1132
        except resource.ResourceNotFoundError:
1133
            self.print_not_found(args.id)
1134
            raise OperationFailureException('Execution %s not found.' % (args.id))
1135
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1136
1137
1138
class ActionExecutionCancelCommand(resource.ResourceCommand):
1139
1140
    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...
1141
        super(ActionExecutionCancelCommand, self).__init__(
1142
            resource, 'cancel', 'Cancel %s.' %
1143
            resource.get_plural_display_name().lower(),
1144
            *args, **kwargs)
1145
1146
        self.parser.add_argument('ids',
1147
                                 nargs='+',
1148
                                 help=('IDs of the %ss to cancel.' %
1149
                                       resource.get_display_name().lower()))
1150
1151
    def run(self, args, **kwargs):
1152
        responses = []
1153
        for execution_id in args.ids:
1154
            response = self.manager.delete_by_id(execution_id)
1155
            responses.append([execution_id, response])
1156
1157
        return responses
1158
1159
    @add_auth_token_to_kwargs_from_cli
1160
    def run_and_print(self, args, **kwargs):
1161
        responses = self.run(args, **kwargs)
1162
1163
        for execution_id, response in responses:
1164
            self._print_result(execution_id=execution_id, response=response)
1165
1166
    def _print_result(self, execution_id, response):
1167
        if response and 'faultstring' in response:
1168
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1169
                                   (self.resource.get_display_name().lower(), execution_id))
1170
1171
        elif response:
1172
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1173
                                                   execution_id)
1174
        else:
1175
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1176
                                                        execution_id)
1177
        print(message)
1178
1179
1180
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1181
    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...
1182
1183
        super(ActionExecutionReRunCommand, self).__init__(
1184
            resource, kwargs.pop('name', 're-run'),
1185
            'A command to re-run a particular action.',
1186
            *args, **kwargs)
1187
1188
        self.parser.add_argument('id', nargs='?',
1189
                                 metavar='id',
1190
                                 help='ID of action execution to re-run ')
1191
        self.parser.add_argument('parameters', nargs='*',
1192
                                 help='List of keyword args, positional args, '
1193
                                      'and optional args for the action.')
1194
        self.parser.add_argument('--tasks', nargs='*',
1195
                                 help='Name of the workflow tasks to re-run.')
1196
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1197
                                 help='Name of the with-items tasks to not reset. This only '
1198
                                      'applies to Mistral workflows. By default, all iterations '
1199
                                      'for with-items tasks is rerun. If no reset, only failed '
1200
                                      ' iterations are rerun.')
1201
        self.parser.add_argument('-a', '--async',
1202
                                 action='store_true', dest='async',
1203
                                 help='Do not wait for action to finish.')
1204
        self.parser.add_argument('-e', '--inherit-env',
1205
                                 action='store_true', dest='inherit_env',
1206
                                 help='Pass all the environment variables '
1207
                                      'which are accessible to the CLI as "env" '
1208
                                      'parameter to the action. Note: Only works '
1209
                                      'with python, local and remote runners.')
1210
        self.parser.add_argument('-h', '--help',
1211
                                 action='store_true', dest='help',
1212
                                 help='Print usage for the given action.')
1213
1214
        self._add_common_options()
1215
1216
    @add_auth_token_to_kwargs_from_cli
1217
    def run(self, args, **kwargs):
1218
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1219
1220
        if not existing_execution:
1221
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1222
                                                 (args.id))
1223
1224
        action_mgr = self.app.client.managers['Action']
1225
        runner_mgr = self.app.client.managers['RunnerType']
1226
        action_exec_mgr = self.app.client.managers['LiveAction']
1227
1228
        action_ref = existing_execution.action['ref']
1229
        action = action_mgr.get_by_ref_or_id(action_ref)
1230
        runner = runner_mgr.get_by_name(action.runner_type)
1231
1232
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1233
                                                                  args=args)
1234
1235
        execution = action_exec_mgr.re_run(execution_id=args.id,
1236
                                           parameters=action_parameters,
1237
                                           tasks=args.tasks,
1238
                                           no_reset=args.no_reset,
1239
                                           **kwargs)
1240
1241
        execution = self._get_execution_result(execution=execution,
1242
                                               action_exec_mgr=action_exec_mgr,
1243
                                               args=args, **kwargs)
1244
1245
        return execution
1246
1247
1248
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1249
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1250
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1251
1252
    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...
1253
        super(ActionExecutionPauseCommand, self).__init__(
1254
            resource, 'pause', 'Pause %s (workflow executions only).' %
1255
            resource.get_plural_display_name().lower(),
1256
            *args, **kwargs)
1257
1258
        self.parser.add_argument('id', nargs='?',
1259
                                 metavar='id',
1260
                                 help='ID of action execution to pause.')
1261
1262
        self._add_common_options()
1263
1264
    @add_auth_token_to_kwargs_from_cli
1265
    def run(self, args, **kwargs):
1266
        return self.manager.pause(args.id)
1267
1268
    @add_auth_token_to_kwargs_from_cli
1269
    def run_and_print(self, args, **kwargs):
1270
        try:
1271
            execution = self.run(args, **kwargs)
1272
1273
            if not args.json and not args.yaml:
1274
                # Include elapsed time for running executions
1275
                execution = format_execution_status(execution)
1276
        except resource.ResourceNotFoundError:
1277
            self.print_not_found(args.id)
1278
            raise OperationFailureException('Execution %s not found.' % (args.id))
1279
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1280
1281
1282
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1283
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1284
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1285
1286
    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...
1287
        super(ActionExecutionResumeCommand, self).__init__(
1288
            resource, 'resume', 'Resume %s (workflow executions only).' %
1289
            resource.get_plural_display_name().lower(),
1290
            *args, **kwargs)
1291
1292
        self.parser.add_argument('id', nargs='?',
1293
                                 metavar='id',
1294
                                 help='ID of action execution to resume.')
1295
1296
        self._add_common_options()
1297
1298
    @add_auth_token_to_kwargs_from_cli
1299
    def run(self, args, **kwargs):
1300
        return self.manager.resume(args.id)
1301
1302
    @add_auth_token_to_kwargs_from_cli
1303
    def run_and_print(self, args, **kwargs):
1304
        try:
1305
            execution = self.run(args, **kwargs)
1306
1307
            if not args.json and not args.yaml:
1308
                # Include elapsed time for running executions
1309
                execution = format_execution_status(execution)
1310
        except resource.ResourceNotFoundError:
1311
            self.print_not_found(args.id)
1312
            raise OperationFailureException('Execution %s not found.' % (args.id))
1313
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1314