Passed
Push — master ( f1fe9e...5c5de8 )
by
unknown
03:44
created

ActionExecutionTailCommand.run_and_print()   F

Complexity

Conditions 10

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 10
c 3
b 0
f 0
dl 0
loc 46
rs 3.2727

How to fix   Complexity   

Complexity

Complex classes like ActionExecutionTailCommand.run_and_print() 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 ResourceNotFoundError
32
from st2client.commands.resource import add_auth_token_to_kwargs_from_cli
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
        self.commands['tail'] = ActionExecutionTailCommand(self.resource, self.app,
987
                                                           self.subparsers, add_help=False)
988
989
990
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled')
991
992
993
class ActionExecutionReadCommand(resource.ResourceCommand):
994
    """
995
    Base class for read / view commands (list and get).
996
    """
997
998
    @classmethod
999
    def _get_exclude_attributes(cls, args):
1000
        """
1001
        Retrieve a list of exclude attributes for particular command line arguments.
1002
        """
1003
        exclude_attributes = []
1004
1005
        result_included = False
1006
        trigger_instance_included = False
1007
1008
        for attr in args.attr:
1009
            # Note: We perform startswith check so we correctly detected child attribute properties
1010
            # (e.g. result, result.stdout, result.stderr, etc.)
1011
            if attr.startswith('result'):
1012
                result_included = True
1013
1014
            if attr.startswith('trigger_instance'):
1015
                trigger_instance_included = True
1016
1017
        if not result_included:
1018
            exclude_attributes.append('result')
1019
        if not trigger_instance_included:
1020
            exclude_attributes.append('trigger_instance')
1021
1022
        return exclude_attributes
1023
1024
1025
class ActionExecutionListCommand(ActionExecutionReadCommand):
1026
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
1027
                          'end_timestamp']
1028
    attribute_transform_functions = {
1029
        'start_timestamp': format_isodate_for_user_timezone,
1030
        'end_timestamp': format_isodate_for_user_timezone,
1031
        'parameters': format_parameters,
1032
        'status': format_status
1033
    }
1034
1035
    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...
1036
        super(ActionExecutionListCommand, self).__init__(
1037
            resource, 'list', 'Get the list of the 50 most recent %s.' %
1038
            resource.get_plural_display_name().lower(),
1039
            *args, **kwargs)
1040
1041
        self.default_limit = 50
1042
        self.resource_name = resource.get_plural_display_name().lower()
1043
        self.group = self.parser.add_argument_group()
1044
        self.parser.add_argument('-n', '--last', type=int, dest='last',
1045
                                 default=self.default_limit,
1046
                                 help=('List N most recent %s.' % self.resource_name))
1047
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
1048
                                 default='descending',
1049
                                 help=('Sort %s by start timestamp, '
1050
                                       'asc|ascending (earliest first) '
1051
                                       'or desc|descending (latest first)' % self.resource_name))
1052
1053
        # Filter options
1054
        self.group.add_argument('--action', help='Action reference to filter the list.')
1055
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
1056
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
1057
                                                  '\'%s\' or \'%s\''
1058
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
1059
        self.group.add_argument('--trigger_instance',
1060
                                help='Trigger instance id to filter the list.')
1061
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
1062
                                 default=None,
1063
                                 help=('Only return executions with timestamp '
1064
                                       'greater than the one provided. '
1065
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1066
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
1067
                                 default=None,
1068
                                 help=('Only return executions with timestamp '
1069
                                       'lower than the one provided. '
1070
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1071
        self.parser.add_argument('-l', '--showall', action='store_true',
1072
                                 help='')
1073
1074
        # Display options
1075
        self.parser.add_argument('-a', '--attr', nargs='+',
1076
                                 default=self.display_attributes,
1077
                                 help=('List of attributes to include in the '
1078
                                       'output. "all" will return all '
1079
                                       'attributes.'))
1080
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
1081
                                 default=None,
1082
                                 help=('Set the width of columns in output.'))
1083
1084
    @add_auth_token_to_kwargs_from_cli
1085
    def run(self, args, **kwargs):
1086
        # Filtering options
1087
        if args.action:
1088
            kwargs['action'] = args.action
1089
        if args.status:
1090
            kwargs['status'] = args.status
1091
        if args.trigger_instance:
1092
            kwargs['trigger_instance'] = args.trigger_instance
1093
        if not args.showall:
1094
            # null is the magic string that translates to does not exist.
1095
            kwargs['parent'] = 'null'
1096
        if args.timestamp_gt:
1097
            kwargs['timestamp_gt'] = args.timestamp_gt
1098
        if args.timestamp_lt:
1099
            kwargs['timestamp_lt'] = args.timestamp_lt
1100
        if args.sort_order:
1101
            if args.sort_order in ['asc', 'ascending']:
1102
                kwargs['sort_asc'] = True
1103
            elif args.sort_order in ['desc', 'descending']:
1104
                kwargs['sort_desc'] = True
1105
1106
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
1107
        # since they are not displayed nor used which speeds the common operation substantially.
1108
        exclude_attributes = self._get_exclude_attributes(args=args)
1109
        exclude_attributes = ','.join(exclude_attributes)
1110
        kwargs['exclude_attributes'] = exclude_attributes
1111
1112
        return self.manager.query_with_count(limit=args.last, **kwargs)
1113
1114
    def run_and_print(self, args, **kwargs):
1115
1116
        result, count = self.run(args, **kwargs)
1117
        instances = format_wf_instances(result)
1118
1119
        if args.json or args.yaml:
1120
            self.print_output(reversed(instances), table.MultiColumnTable,
1121
                              attributes=args.attr, widths=args.width,
1122
                              json=args.json,
1123
                              yaml=args.yaml,
1124
                              attribute_transform_functions=self.attribute_transform_functions)
1125
1126
        else:
1127
            # Include elapsed time for running executions
1128
            instances = format_execution_statuses(instances)
1129
            self.print_output(reversed(instances), table.MultiColumnTable,
1130
                              attributes=args.attr, widths=args.width,
1131
                              attribute_transform_functions=self.attribute_transform_functions)
1132
1133
            if args.last and count and count > args.last:
1134
                table.SingleRowTable.note_box(self.resource_name, args.last)
1135
1136
1137
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1138
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1139
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1140
1141
    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...
1142
        super(ActionExecutionGetCommand, self).__init__(
1143
            resource, 'get',
1144
            'Get individual %s.' % resource.get_display_name().lower(),
1145
            *args, **kwargs)
1146
1147
        self.parser.add_argument('id',
1148
                                 help=('ID of the %s.' %
1149
                                       resource.get_display_name().lower()))
1150
1151
        self._add_common_options()
1152
1153
    @add_auth_token_to_kwargs_from_cli
1154
    def run(self, args, **kwargs):
1155
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1156
        # requested by user either via "--attr" flag or by default.
1157
        exclude_attributes = self._get_exclude_attributes(args=args)
1158
        exclude_attributes = ','.join(exclude_attributes)
1159
1160
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1161
1162
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1163
        return execution
1164
1165
    @add_auth_token_to_kwargs_from_cli
1166
    def run_and_print(self, args, **kwargs):
1167
        try:
1168
            execution = self.run(args, **kwargs)
1169
1170
            if not args.json and not args.yaml:
1171
                # Include elapsed time for running executions
1172
                execution = format_execution_status(execution)
1173
        except resource.ResourceNotFoundError:
1174
            self.print_not_found(args.id)
1175
            raise ResourceNotFoundError('Execution with id %s not found.' % (args.id))
1176
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1177
1178
1179
class ActionExecutionCancelCommand(resource.ResourceCommand):
1180
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
        super(ActionExecutionCancelCommand, self).__init__(
1183
            resource, 'cancel', 'Cancel %s.' %
1184
            resource.get_plural_display_name().lower(),
1185
            *args, **kwargs)
1186
1187
        self.parser.add_argument('ids',
1188
                                 nargs='+',
1189
                                 help=('IDs of the %ss to cancel.' %
1190
                                       resource.get_display_name().lower()))
1191
1192
    def run(self, args, **kwargs):
1193
        responses = []
1194
        for execution_id in args.ids:
1195
            response = self.manager.delete_by_id(execution_id)
1196
            responses.append([execution_id, response])
1197
1198
        return responses
1199
1200
    @add_auth_token_to_kwargs_from_cli
1201
    def run_and_print(self, args, **kwargs):
1202
        responses = self.run(args, **kwargs)
1203
1204
        for execution_id, response in responses:
1205
            self._print_result(execution_id=execution_id, response=response)
1206
1207
    def _print_result(self, execution_id, response):
1208
        if response and 'faultstring' in response:
1209
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1210
                                   (self.resource.get_display_name().lower(), execution_id))
1211
1212
        elif response:
1213
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1214
                                                   execution_id)
1215
        else:
1216
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1217
                                                        execution_id)
1218
        print(message)
1219
1220
1221
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1222
    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...
1223
1224
        super(ActionExecutionReRunCommand, self).__init__(
1225
            resource, kwargs.pop('name', 're-run'),
1226
            'A command to re-run a particular action.',
1227
            *args, **kwargs)
1228
1229
        self.parser.add_argument('id', nargs='?',
1230
                                 metavar='id',
1231
                                 help='ID of action execution to re-run ')
1232
        self.parser.add_argument('parameters', nargs='*',
1233
                                 help='List of keyword args, positional args, '
1234
                                      'and optional args for the action.')
1235
        self.parser.add_argument('--tasks', nargs='*',
1236
                                 help='Name of the workflow tasks to re-run.')
1237
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1238
                                 help='Name of the with-items tasks to not reset. This only '
1239
                                      'applies to Mistral workflows. By default, all iterations '
1240
                                      'for with-items tasks is rerun. If no reset, only failed '
1241
                                      ' iterations are rerun.')
1242
        self.parser.add_argument('-a', '--async',
1243
                                 action='store_true', dest='async',
1244
                                 help='Do not wait for action to finish.')
1245
        self.parser.add_argument('-e', '--inherit-env',
1246
                                 action='store_true', dest='inherit_env',
1247
                                 help='Pass all the environment variables '
1248
                                      'which are accessible to the CLI as "env" '
1249
                                      'parameter to the action. Note: Only works '
1250
                                      'with python, local and remote runners.')
1251
        self.parser.add_argument('-h', '--help',
1252
                                 action='store_true', dest='help',
1253
                                 help='Print usage for the given action.')
1254
1255
        self._add_common_options()
1256
1257
    @add_auth_token_to_kwargs_from_cli
1258
    def run(self, args, **kwargs):
1259
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1260
1261
        if not existing_execution:
1262
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1263
                                                 (args.id))
1264
1265
        action_mgr = self.app.client.managers['Action']
1266
        runner_mgr = self.app.client.managers['RunnerType']
1267
        action_exec_mgr = self.app.client.managers['LiveAction']
1268
1269
        action_ref = existing_execution.action['ref']
1270
        action = action_mgr.get_by_ref_or_id(action_ref)
1271
        runner = runner_mgr.get_by_name(action.runner_type)
1272
1273
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1274
                                                                  args=args)
1275
1276
        execution = action_exec_mgr.re_run(execution_id=args.id,
1277
                                           parameters=action_parameters,
1278
                                           tasks=args.tasks,
1279
                                           no_reset=args.no_reset,
1280
                                           **kwargs)
1281
1282
        execution = self._get_execution_result(execution=execution,
1283
                                               action_exec_mgr=action_exec_mgr,
1284
                                               args=args, **kwargs)
1285
1286
        return execution
1287
1288
1289
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1290
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1291
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1292
1293
    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...
1294
        super(ActionExecutionPauseCommand, self).__init__(
1295
            resource, 'pause', 'Pause %s (workflow executions only).' %
1296
            resource.get_plural_display_name().lower(),
1297
            *args, **kwargs)
1298
1299
        self.parser.add_argument('id', nargs='?',
1300
                                 metavar='id',
1301
                                 help='ID of action execution to pause.')
1302
1303
        self._add_common_options()
1304
1305
    @add_auth_token_to_kwargs_from_cli
1306
    def run(self, args, **kwargs):
1307
        return self.manager.pause(args.id)
1308
1309
    @add_auth_token_to_kwargs_from_cli
1310
    def run_and_print(self, args, **kwargs):
1311
        try:
1312
            execution = self.run(args, **kwargs)
1313
1314
            if not args.json and not args.yaml:
1315
                # Include elapsed time for running executions
1316
                execution = format_execution_status(execution)
1317
        except resource.ResourceNotFoundError:
1318
            self.print_not_found(args.id)
1319
            raise ResourceNotFoundError('Execution  with id %s not found.' % (args.id))
1320
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1321
1322
1323
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1324
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1325
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1326
1327
    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...
1328
        super(ActionExecutionResumeCommand, self).__init__(
1329
            resource, 'resume', 'Resume %s (workflow executions only).' %
1330
            resource.get_plural_display_name().lower(),
1331
            *args, **kwargs)
1332
1333
        self.parser.add_argument('id', nargs='?',
1334
                                 metavar='id',
1335
                                 help='ID of action execution to resume.')
1336
1337
        self._add_common_options()
1338
1339
    @add_auth_token_to_kwargs_from_cli
1340
    def run(self, args, **kwargs):
1341
        return self.manager.resume(args.id)
1342
1343
    @add_auth_token_to_kwargs_from_cli
1344
    def run_and_print(self, args, **kwargs):
1345
        try:
1346
            execution = self.run(args, **kwargs)
1347
1348
            if not args.json and not args.yaml:
1349
                # Include elapsed time for running executions
1350
                execution = format_execution_status(execution)
1351
        except resource.ResourceNotFoundError:
1352
            self.print_not_found(args.id)
1353
            raise ResourceNotFoundError('Execution %s not found.' % (args.id))
1354
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1355
1356
1357
class ActionExecutionTailCommand(resource.ResourceCommand):
1358
    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...
1359
        super(ActionExecutionTailCommand, self).__init__(
1360
            resource, kwargs.pop('name', 'tail'),
1361
            'A command to tail output of a particular execution.',
1362
            *args, **kwargs)
1363
1364
        self.parser.add_argument('id', nargs='?',
1365
                                 metavar='id',
1366
                                 default='last',
1367
                                 help='ID of action execution to tail.')
1368
        self.parser.add_argument('--type', dest='output_type', action='store',
1369
                                 help=('Type of output to tail for. If not provided, '
1370
                                      'defaults to all.'))
1371
        self.parser.add_argument('-h', '--help',
1372
                                 action='store_true', dest='help',
1373
                                 help='Print usage for the given command.')
1374
1375
    def run(self, args, **kwargs):
1376
        pass
1377
1378
    @add_auth_token_to_kwargs_from_cli
1379
    def run_and_print(self, args, **kwargs):
1380
        execution_id = args.id
1381
        output_type = getattr(args, 'output_type', None)
1382
1383
        # Special case for id "last"
1384
        if execution_id == 'last':
1385
            executions = self.manager.query(limit=1)
1386
            execution = executions[0] if executions else None
1387
            execution_id = execution.id
1388
        else:
1389
            execution = self.manager.get_by_id(execution_id, **kwargs)
1390
1391
        if not execution:
1392
            raise ResourceNotFoundError('Execution  with id %s not found.' % (args.id))
1393
1394
        # Execution has already finished
1395
        if execution.status in LIVEACTION_COMPLETED_STATES:
1396
            output = self.manager.get_output(execution_id=execution_id, output_type=output_type)
1397
            print(output)
1398
            print('Execution %s has completed.' % (execution_id))
1399
            return
1400
1401
        stream_mgr = self.app.client.managers['Stream']
1402
        events = ['st2.execution__update', 'st2.execution.output__create']
1403
1404
        for event in stream_mgr.listen(events, **kwargs):
1405
            status = event.get('status', None)
1406
            is_execution_event = status is not None
1407
1408
            if is_execution_event:
1409
                if status in LIVEACTION_COMPLETED_STATES:
1410
                    # Execution has completed
1411
                    print('Execution %s has completed.' % (execution_id))
1412
                    break
1413
                else:
1414
                    # We don't care about other execution events
1415
                    continue
1416
1417
            # Filter on output_type if provided
1418
            event_output_type = event.get('output_type', None)
1419
            if output_type and event_output_type != output_type:
1420
                continue
1421
1422
            sys.stdout.write('[%s][%s] %s' % (event['timestamp'], event['output_type'],
1423
                                              event['data']))
1424