Passed
Push — master ( 6aeca2...dec5f2 )
by
unknown
03:57
created

ActionExecutionTailCommand.run()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 2
rs 10
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
            self.print_output('\nTo view output in real-time, execute:\n st2 execution '
262
                              'tail %s' % (execution.id), six.text_type)
263
        else:
264
            self._print_execution_details(execution=execution, args=args, **kwargs)
265
266
        if execution.status == 'failed':
267
            # Exit with non zero if the action has failed
268
            sys.exit(1)
269
270
    def _add_common_options(self):
271
        root_arg_grp = self.parser.add_mutually_exclusive_group()
272
273
        # Display options
274
        task_list_arg_grp = root_arg_grp.add_argument_group()
275
        task_list_arg_grp.add_argument('--raw', action='store_true',
276
                                       help='Raw output, don\'t show sub-tasks for workflows.')
277
        task_list_arg_grp.add_argument('--show-tasks', action='store_true',
278
                                       help='Whether to show sub-tasks of an execution.')
279
        task_list_arg_grp.add_argument('--depth', type=int, default=-1,
280
                                       help='Depth to which to show sub-tasks. \
281
                                             By default all are shown.')
282
        task_list_arg_grp.add_argument('-w', '--width', nargs='+', type=int, default=None,
283
                                       help='Set the width of columns in output.')
284
285
        execution_details_arg_grp = root_arg_grp.add_mutually_exclusive_group()
286
287
        detail_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
288
        detail_arg_grp.add_argument('--attr', nargs='+',
289
                                    default=['id', 'status', 'parameters', 'result'],
290
                                    help=('List of attributes to include in the '
291
                                          'output. "all" or unspecified will '
292
                                          'return all attributes.'))
293
        detail_arg_grp.add_argument('-d', '--detail', action='store_true',
294
                                    help='Display full detail of the execution in table format.')
295
296
        result_arg_grp = execution_details_arg_grp.add_mutually_exclusive_group()
297
        result_arg_grp.add_argument('-k', '--key',
298
                                    help=('If result is type of JSON, then print specific '
299
                                          'key-value pair; dot notation for nested JSON is '
300
                                          'supported.'))
301
302
        # Other options
303
        detail_arg_grp.add_argument('--tail', action='store_true',
304
                                    help='Automatically start tailing new execution.')
305
306
        return root_arg_grp
307
308
    def _print_execution_details(self, execution, args, **kwargs):
309
        """
310
        Print the execution detail to stdout.
311
312
        This method takes into account if an executed action was workflow or not
313
        and formats the output accordingly.
314
        """
315
        runner_type = execution.action.get('runner_type', 'unknown')
316
        is_workflow_action = runner_type in WORKFLOW_RUNNER_TYPES
317
318
        show_tasks = getattr(args, 'show_tasks', False)
319
        raw = getattr(args, 'raw', False)
320
        detail = getattr(args, 'detail', False)
321
        key = getattr(args, 'key', None)
322
        attr = getattr(args, 'attr', [])
323
324
        if show_tasks and not is_workflow_action:
325
            raise ValueError('--show-tasks option can only be used with workflow actions')
326
327
        if not raw and not detail and (show_tasks or is_workflow_action):
328
            self._run_and_print_child_task_list(execution=execution, args=args, **kwargs)
329
        else:
330
            instance = execution
331
332
            if detail:
333
                formatter = table.PropertyValueTable
334
            else:
335
                formatter = execution_formatter.ExecutionResult
336
337
            if detail:
338
                options = {'attributes': copy.copy(self.display_attributes)}
339
            elif key:
340
                options = {'attributes': ['result.%s' % (key)], 'key': key}
341
            else:
342
                options = {'attributes': attr}
343
344
            options['json'] = args.json
345
            options['attribute_transform_functions'] = self.attribute_transform_functions
346
            self.print_output(instance, formatter, **options)
347
348
    def _run_and_print_child_task_list(self, execution, args, **kwargs):
349
        action_exec_mgr = self.app.client.managers['LiveAction']
350
351
        instance = execution
352
        options = {'attributes': ['id', 'action.ref', 'parameters', 'status', 'start_timestamp',
353
                                  'end_timestamp']}
354
        options['json'] = args.json
355
        options['attribute_transform_functions'] = self.attribute_transform_functions
356
        formatter = execution_formatter.ExecutionResult
357
358
        kwargs['depth'] = args.depth
359
        child_instances = action_exec_mgr.get_property(execution.id, 'children', **kwargs)
360
        child_instances = self._format_child_instances(child_instances, execution.id)
361
        child_instances = format_execution_statuses(child_instances)
362
363
        if not child_instances:
364
            # No child error, there might be a global error, include result in the output
365
            options['attributes'].append('result')
366
367
        status_index = options['attributes'].index('status')
368
369
        if hasattr(instance, 'result') and isinstance(instance.result, dict):
370
            tasks = instance.result.get('tasks', [])
371
        else:
372
            tasks = []
373
374
        # On failure we also want to include error message and traceback at the top level
375
        if instance.status == 'failed':
376
            top_level_error, top_level_traceback = self._get_top_level_error(live_action=instance)
377
378
            if len(tasks) >= 1:
379
                task_error, task_traceback = self._get_task_error(task=tasks[-1])
380
            else:
381
                task_error, task_traceback = None, None
382
383
            if top_level_error:
384
                # Top-level error
385
                instance.error = top_level_error
386
                instance.traceback = top_level_traceback
387
                instance.result = 'See error and traceback.'
388
                options['attributes'].insert(status_index + 1, 'error')
389
                options['attributes'].insert(status_index + 2, 'traceback')
390
            elif task_error:
391
                # Task error
392
                instance.error = task_error
393
                instance.traceback = task_traceback
394
                instance.result = 'See error and traceback.'
395
                instance.failed_on = tasks[-1].get('name', 'unknown')
396
                options['attributes'].insert(status_index + 1, 'error')
397
                options['attributes'].insert(status_index + 2, 'traceback')
398
                options['attributes'].insert(status_index + 3, 'failed_on')
399
400
        # Include result on the top-level object so user doesn't need to issue another command to
401
        # see the result
402
        if len(tasks) >= 1:
403
            task_result = self._get_task_result(task=tasks[-1])
404
405
            if task_result:
406
                instance.result_task = tasks[-1].get('name', 'unknown')
407
                options['attributes'].insert(status_index + 1, 'result_task')
408
                options['attributes'].insert(status_index + 2, 'result')
409
                instance.result = task_result
410
411
        # print root task
412
        self.print_output(instance, formatter, **options)
413
414
        # print child tasks
415
        if child_instances:
416
            self.print_output(child_instances, table.MultiColumnTable,
417
                              attributes=['id', 'status', 'task', 'action', 'start_timestamp'],
418
                              widths=args.width, json=args.json,
419
                              yaml=args.yaml,
420
                              attribute_transform_functions=self.attribute_transform_functions)
421
422
    def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
423
        pending_statuses = [
424
            LIVEACTION_STATUS_REQUESTED,
425
            LIVEACTION_STATUS_SCHEDULED,
426
            LIVEACTION_STATUS_RUNNING,
427
            LIVEACTION_STATUS_CANCELING
428
        ]
429
430
        if args.tail:
431
            # Start tailing new execution
432
            print('Tailing execution "%s"' % (str(execution.id)))
433
            execution_manager = self.manager
434
            stream_manager = self.app.client.managers['Stream']
435
            ActionExecutionTailCommand.tail_execution(execution=execution,
436
                                                      execution_manager=execution_manager,
437
                                                      stream_manager=stream_manager,
438
                                                      **kwargs)
439
440
            execution = action_exec_mgr.get_by_id(execution.id, **kwargs)
441
            print('')
442
            return execution
443
444
        if not args.async:
445
            while execution.status in pending_statuses:
446
                time.sleep(self.poll_interval)
447
                if not args.json and not args.yaml:
448
                    sys.stdout.write('.')
449
                    sys.stdout.flush()
450
                execution = action_exec_mgr.get_by_id(execution.id, **kwargs)
451
452
            sys.stdout.write('\n')
453
454
            if execution.status == LIVEACTION_STATUS_CANCELED:
455
                return execution
456
457
        return execution
458
459
    def _get_top_level_error(self, live_action):
460
        """
461
        Retrieve a top level workflow error.
462
463
        :return: (error, traceback)
464
        """
465
        if isinstance(live_action.result, dict):
466
            error = live_action.result.get('error', None)
467
            traceback = live_action.result.get('traceback', None)
468
        else:
469
            error = "See result"
470
            traceback = "See result"
471
472
        return error, traceback
473
474
    def _get_task_error(self, task):
475
        """
476
        Retrieve error message from the provided task.
477
478
        :return: (error, traceback)
479
        """
480
        if not task:
481
            return None, None
482
483
        result = task['result']
484
485
        if isinstance(result, dict):
486
            stderr = result.get('stderr', None)
487
            error = result.get('error', None)
488
            traceback = result.get('traceback', None)
489
            error = error if error else stderr
490
        else:
491
            stderr = None
492
            error = None
493
            traceback = None
494
495
        return error, traceback
496
497
    def _get_task_result(self, task):
498
        if not task:
499
            return None
500
501
        return task['result']
502
503
    def _get_action_parameters_from_args(self, action, runner, args):
504
        """
505
        Build a dictionary with parameters which will be passed to the action by
506
        parsing parameters passed to the CLI.
507
508
        :param args: CLI argument.
509
        :type args: ``object``
510
511
        :rtype: ``dict``
512
        """
513
        action_ref_or_id = action.ref
514
515
        def read_file(file_path):
516
            if not os.path.exists(file_path):
517
                raise ValueError('File "%s" doesn\'t exist' % (file_path))
518
519
            if not os.path.isfile(file_path):
520
                raise ValueError('"%s" is not a file' % (file_path))
521
522
            with open(file_path, 'rb') as fp:
523
                content = fp.read()
524
525
            return content
526
527
        def transform_object(value):
528
            # Also support simple key1=val1,key2=val2 syntax
529
            if value.startswith('{'):
530
                # Assume it's JSON
531
                result = value = json.loads(value)
532
            else:
533
                pairs = value.split(',')
534
535
                result = {}
536
                for pair in pairs:
537
                    split = pair.split('=', 1)
538
539
                    if len(split) != 2:
540
                        continue
541
542
                    key, value = split
543
                    result[key] = value
544
            return result
545
546
        def transform_array(value, action_params=None):
547
            action_params = action_params or {}
548
549
            # Sometimes an array parameter only has a single element:
550
            #
551
            #     i.e. "st2 run foopack.fooaction arrayparam=51"
552
            #
553
            # Normally, json.loads would throw an exception, and the split method
554
            # would be used. However, since this is an int, not only would
555
            # splitting not work, but json.loads actually treats this as valid JSON,
556
            # but as an int, not an array. This causes a mismatch when the API is called.
557
            #
558
            # We want to try to handle this first, so it doesn't get accidentally
559
            # sent to the API as an int, instead of an array of single-element int.
560
            try:
561
                # Force this to be a list containing the single int, then
562
                # cast the whole thing to string so json.loads can handle it
563
                value = str([int(value)])
564
            except ValueError:
565
                # Original value wasn't an int, so just let it continue
566
                pass
567
568
            # At this point, the input is either a a "json.loads"-able construct
569
            # like [1, 2, 3], or even [1], or it is a comma-separated list,
570
            # Try both, in that order.
571
            try:
572
                result = json.loads(value)
573
            except ValueError:
574
                result = [v.strip() for v in value.split(',')]
575
576
            # When each values in this array represent dict type, this converts
577
            # the 'result' to the dict type value.
578
            if all([isinstance(x, str) and ':' in x for x in result]):
579
                result_dict = {}
580
                for (k, v) in [x.split(':') for x in result]:
581
                    # To parse values using the 'transformer' according to the type which is
582
                    # specified in the action metadata, calling 'normalize' method recursively.
583
                    if 'properties' in action_params and k in action_params['properties']:
584
                        result_dict[k] = normalize(k, v, action_params['properties'])
585
                    else:
586
                        result_dict[k] = v
587
                return [result_dict]
588
589
            return result
590
591
        transformer = {
592
            'array': transform_array,
593
            'boolean': (lambda x: ast.literal_eval(x.capitalize())),
594
            'integer': int,
595
            'number': float,
596
            'object': transform_object,
597
            'string': str
598
        }
599
600
        def get_param_type(key, action_params=None):
601
            action_params = action_params or action.parameters
602
603
            param = None
604
            if key in runner.runner_parameters:
605
                param = runner.runner_parameters[key]
606
            elif key in action_params:
607
                param = action_params[key]
608
609
            if param:
610
                return param['type']
611
612
            return None
613
614
        def normalize(name, value, action_params=None):
615
            """ The desired type is contained in the action meta-data, so we can look that up
616
                and call the desired "caster" function listed in the "transformer" dict
617
            """
618
            action_params = action_params or action.parameters
619
620
            # By default, this method uses a parameter which is defined in the action metadata.
621
            # This method assume to be called recursively for parsing values in an array of objects
622
            # type value according to the nested action metadata definition.
623
            #
624
            # This is a best practice to pass a list value as default argument to prevent
625
            # unforeseen consequence by being created a persistent object.
626
627
            # Users can also specify type for each array parameter inside an action metadata
628
            # (items: type: int for example) and this information is available here so we could
629
            # also leverage that to cast each array item to the correct type.
630
            param_type = get_param_type(name, action_params)
631
            if param_type == 'array' and name in action_params:
632
                return transformer[param_type](value, action_params[name])
633
            elif param_type:
634
                return transformer[param_type](value)
635
636
            return value
637
638
        result = {}
639
640
        if not args.parameters:
641
            return result
642
643
        for idx in range(len(args.parameters)):
644
            arg = args.parameters[idx]
645
            if '=' in arg:
646
                k, v = arg.split('=', 1)
647
648
                # Attribute for files are prefixed with "@"
649
                if k.startswith('@'):
650
                    k = k[1:]
651
                    is_file = True
652
                else:
653
                    is_file = False
654
655
                try:
656
                    if is_file:
657
                        # Files are handled a bit differently since we ship the content
658
                        # over the wire
659
                        file_path = os.path.normpath(pjoin(os.getcwd(), v))
660
                        file_name = os.path.basename(file_path)
661
                        content = read_file(file_path=file_path)
662
663
                        if action_ref_or_id == 'core.http':
664
                            # Special case for http runner
665
                            result['_file_name'] = file_name
666
                            result['file_content'] = content
667
                        else:
668
                            result[k] = content
669
                    else:
670
                        # This permits multiple declarations of argument only in the array type.
671
                        if get_param_type(k) == 'array' and k in result:
672
                            result[k] += normalize(k, v)
673
                        else:
674
                            result[k] = normalize(k, v)
675
676
                except Exception as e:
677
                    # TODO: Move transformers in a separate module and handle
678
                    # exceptions there
679
                    if 'malformed string' in str(e):
680
                        message = ('Invalid value for boolean parameter. '
681
                                   'Valid values are: true, false')
682
                        raise ValueError(message)
683
                    else:
684
                        raise e
685
            else:
686
                result['cmd'] = ' '.join(args.parameters[idx:])
687
                break
688
689
        # Special case for http runner
690
        if 'file_content' in result:
691
            if 'method' not in result:
692
                # Default to POST if a method is not provided
693
                result['method'] = 'POST'
694
695
            if 'file_name' not in result:
696
                # File name not provided, use default file name
697
                result['file_name'] = result['_file_name']
698
699
            del result['_file_name']
700
701
        if args.inherit_env:
702
            result['env'] = self._get_inherited_env_vars()
703
704
        return result
705
706
    @add_auth_token_to_kwargs_from_cli
707
    def _print_help(self, args, **kwargs):
708
        # Print appropriate help message if the help option is given.
709
        action_mgr = self.app.client.managers['Action']
710
        action_exec_mgr = self.app.client.managers['LiveAction']
711
712
        if args.help:
713
            action_ref_or_id = getattr(args, 'ref_or_id', None)
714
            action_exec_id = getattr(args, 'id', None)
715
716
            if action_exec_id and not action_ref_or_id:
717
                action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs)
718
                args.ref_or_id = action_exec.action
719
720
            if action_ref_or_id:
721
                try:
722
                    action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs)
723
                    if not action:
724
                        raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id)
725
                    runner_mgr = self.app.client.managers['RunnerType']
726
                    runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
727
                    parameters, required, optional, _ = self._get_params_types(runner,
728
                                                                               action)
729
                    print('')
730
                    print(textwrap.fill(action.description))
731
                    print('')
732
                    if required:
733
                        required = self._sort_parameters(parameters=parameters,
734
                                                         names=required)
735
736
                        print('Required Parameters:')
737
                        [self._print_param(name, parameters.get(name))
738
                            for name in required]
739
                    if optional:
740
                        optional = self._sort_parameters(parameters=parameters,
741
                                                         names=optional)
742
743
                        print('Optional Parameters:')
744
                        [self._print_param(name, parameters.get(name))
745
                            for name in optional]
746
                except resource.ResourceNotFoundError:
747
                    print(('Action "%s" is not found. ' % args.ref_or_id) +
748
                          'Use "st2 action list" to see the list of available actions.')
749
                except Exception as e:
750
                    print('ERROR: Unable to print help for action "%s". %s' %
751
                          (args.ref_or_id, e))
752
            else:
753
                self.parser.print_help()
754
            return True
755
        return False
756
757
    @staticmethod
758
    def _print_param(name, schema):
759
        if not schema:
760
            raise ValueError('Missing schema for parameter "%s"' % (name))
761
762
        wrapper = textwrap.TextWrapper(width=78)
763
        wrapper.initial_indent = ' ' * 4
764
        wrapper.subsequent_indent = wrapper.initial_indent
765
        print(wrapper.fill(name))
766
        wrapper.initial_indent = ' ' * 8
767
        wrapper.subsequent_indent = wrapper.initial_indent
768
        if 'description' in schema and schema['description']:
769
            print(wrapper.fill(schema['description']))
770
        if 'type' in schema and schema['type']:
771
            print(wrapper.fill('Type: %s' % schema['type']))
772
        if 'enum' in schema and schema['enum']:
773
            print(wrapper.fill('Enum: %s' % ', '.join(schema['enum'])))
774
        if 'default' in schema and schema['default'] is not None:
775
            print(wrapper.fill('Default: %s' % schema['default']))
776
        print('')
777
778
    @staticmethod
779
    def _get_params_types(runner, action):
780
        runner_params = runner.runner_parameters
781
        action_params = action.parameters
782
        parameters = copy.copy(runner_params)
783
        parameters.update(copy.copy(action_params))
784
        required = set([k for k, v in six.iteritems(parameters) if v.get('required')])
785
786
        def is_immutable(runner_param_meta, action_param_meta):
787
            # If runner sets a param as immutable, action cannot override that.
788
            if runner_param_meta.get('immutable', False):
789
                return True
790
            else:
791
                return action_param_meta.get('immutable', False)
792
793
        immutable = set()
794
        for param in parameters.keys():
795
            if is_immutable(runner_params.get(param, {}),
796
                            action_params.get(param, {})):
797
                immutable.add(param)
798
799
        required = required - immutable
800
        optional = set(parameters.keys()) - required - immutable
801
802
        return parameters, required, optional, immutable
803
804
    def _format_child_instances(self, children, parent_id):
805
        '''
806
        The goal of this method is to add an indent at every level. This way the
807
        WF is represented as a tree structure while in a list. For the right visuals
808
        representation the list must be a DF traversal else the idents will end up
809
        looking strange.
810
        '''
811
        # apply basic WF formating first.
812
        children = format_wf_instances(children)
813
        # setup a depth lookup table
814
        depth = {parent_id: 0}
815
        result = []
816
        # main loop that indents each entry correctly
817
        for child in children:
818
            # make sure child.parent is in depth and while at it compute the
819
            # right depth for indentation purposes.
820
            if child.parent not in depth:
821
                parent = None
822
                for instance in children:
823
                    if WF_PREFIX in instance.id:
824
                        instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):]
825
                    else:
826
                        instance_id = instance.id
827
                    if instance_id == child.parent:
828
                        parent = instance
829
                if parent and parent.parent and parent.parent in depth:
830
                    depth[child.parent] = depth[parent.parent] + 1
831
                else:
832
                    depth[child.parent] = 0
833
            # now ident for the right visuals
834
            child.id = INDENT_CHAR * depth[child.parent] + child.id
835
            result.append(self._format_for_common_representation(child))
836
        return result
837
838
    def _format_for_common_representation(self, task):
839
        '''
840
        Formats a task for common representation between mistral and action-chain.
841
        '''
842
        # This really needs to be better handled on the back-end but that would be a bigger
843
        # change so handling in cli.
844
        context = getattr(task, 'context', None)
845
        if context and 'chain' in context:
846
            task_name_key = 'context.chain.name'
847
        elif context and 'mistral' in context:
848
            task_name_key = 'context.mistral.task_name'
849
        # Use LiveAction as the object so that the formatter lookup does not change.
850
        # AKA HACK!
851
        return models.action.LiveAction(**{
852
            'id': task.id,
853
            'status': task.status,
854
            'task': jsutil.get_value(vars(task), task_name_key),
855
            'action': task.action.get('ref', None),
856
            'start_timestamp': task.start_timestamp,
857
            'end_timestamp': getattr(task, 'end_timestamp', None)
858
        })
859
860
    def _sort_parameters(self, parameters, names):
861
        """
862
        Sort a provided list of action parameters.
863
864
        :type parameters: ``list``
865
        :type names: ``list`` or ``set``
866
        """
867
        sorted_parameters = sorted(names, key=lambda name:
868
                                   self._get_parameter_sort_value(
869
                                       parameters=parameters,
870
                                       name=name))
871
872
        return sorted_parameters
873
874
    def _get_parameter_sort_value(self, parameters, name):
875
        """
876
        Return a value which determines sort order for a particular parameter.
877
878
        By default, parameters are sorted using "position" parameter attribute.
879
        If this attribute is not available, parameter is sorted based on the
880
        name.
881
        """
882
        parameter = parameters.get(name, None)
883
884
        if not parameter:
885
            return None
886
887
        sort_value = parameter.get('position', name)
888
        return sort_value
889
890
    def _get_inherited_env_vars(self):
891
        env_vars = os.environ.copy()
892
893
        for var_name in ENV_VARS_BLACKLIST:
894
            if var_name.lower() in env_vars:
895
                del env_vars[var_name.lower()]
896
            if var_name.upper() in env_vars:
897
                del env_vars[var_name.upper()]
898
899
        return env_vars
900
901
902
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
903
    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...
904
905
        super(ActionRunCommand, self).__init__(
906
            resource, kwargs.pop('name', 'execute'),
907
            'Invoke an action manually.',
908
            *args, **kwargs)
909
910
        self.parser.add_argument('ref_or_id', nargs='?',
911
                                 metavar='ref-or-id',
912
                                 help='Action reference (pack.action_name) ' +
913
                                 'or ID of the action.')
914
        self.parser.add_argument('parameters', nargs='*',
915
                                 help='List of keyword args, positional args, '
916
                                      'and optional args for the action.')
917
918
        self.parser.add_argument('-h', '--help',
919
                                 action='store_true', dest='help',
920
                                 help='Print usage for the given action.')
921
922
        self._add_common_options()
923
924
        if self.name in ['run', 'execute']:
925
            self.parser.add_argument('--trace-tag', '--trace_tag',
926
                                     help='A trace tag string to track execution later.',
927
                                     dest='trace_tag', required=False)
928
            self.parser.add_argument('--trace-id',
929
                                     help='Existing trace id for this execution.',
930
                                     dest='trace_id', required=False)
931
            self.parser.add_argument('-a', '--async',
932
                                     action='store_true', dest='async',
933
                                     help='Do not wait for action to finish.')
934
            self.parser.add_argument('-e', '--inherit-env',
935
                                     action='store_true', dest='inherit_env',
936
                                     help='Pass all the environment variables '
937
                                          'which are accessible to the CLI as "env" '
938
                                          'parameter to the action. Note: Only works '
939
                                          'with python, local and remote runners.')
940
            self.parser.add_argument('-u', '--user', type=str, default=None,
941
                                           help='User under which to run the action (admins only).')
942
943
        if self.name == 'run':
944
            self.parser.set_defaults(async=False)
945
        else:
946
            self.parser.set_defaults(async=True)
947
948
    @add_auth_token_to_kwargs_from_cli
949
    def run(self, args, **kwargs):
950
        if not args.ref_or_id:
951
            self.parser.error('Missing action reference or id')
952
953
        action = self.get_resource(args.ref_or_id, **kwargs)
954
        if not action:
955
            raise resource.ResourceNotFoundError('Action "%s" cannot be found.'
956
                                                 % (args.ref_or_id))
957
958
        runner_mgr = self.app.client.managers['RunnerType']
959
        runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
960
        if not runner:
961
            raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.'
962
                                                 % (action.runner_type, action.name))
963
964
        action_ref = '.'.join([action.pack, action.name])
965
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
966
                                                                  args=args)
967
968
        execution = models.LiveAction()
969
        execution.action = action_ref
970
        execution.parameters = action_parameters
971
        execution.user = args.user
972
973
        if not args.trace_id and args.trace_tag:
974
            execution.context = {'trace_context': {'trace_tag': args.trace_tag}}
975
976
        if args.trace_id:
977
            execution.context = {'trace_context': {'id_': args.trace_id}}
978
979
        action_exec_mgr = self.app.client.managers['LiveAction']
980
981
        execution = action_exec_mgr.create(execution, **kwargs)
982
        execution = self._get_execution_result(execution=execution,
983
                                               action_exec_mgr=action_exec_mgr,
984
                                               args=args, **kwargs)
985
        return execution
986
987
988
class ActionExecutionBranch(resource.ResourceBranch):
989
990
    def __init__(self, description, app, subparsers, parent_parser=None):
991
        super(ActionExecutionBranch, self).__init__(
992
            models.LiveAction, description, app, subparsers,
993
            parent_parser=parent_parser, read_only=True,
994
            commands={'list': ActionExecutionListCommand,
995
                      'get': ActionExecutionGetCommand})
996
997
        # Register extended commands
998
        self.commands['re-run'] = ActionExecutionReRunCommand(
999
            self.resource, self.app, self.subparsers, add_help=False)
1000
        self.commands['cancel'] = ActionExecutionCancelCommand(
1001
            self.resource, self.app, self.subparsers, add_help=True)
1002
        self.commands['pause'] = ActionExecutionPauseCommand(
1003
            self.resource, self.app, self.subparsers, add_help=True)
1004
        self.commands['resume'] = ActionExecutionResumeCommand(
1005
            self.resource, self.app, self.subparsers, add_help=True)
1006
        self.commands['tail'] = ActionExecutionTailCommand(self.resource, self.app,
1007
                                                           self.subparsers,
1008
                                                           add_help=True)
1009
1010
1011
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceling',
1012
                                 'canceled')
1013
1014
1015
class ActionExecutionReadCommand(resource.ResourceCommand):
1016
    """
1017
    Base class for read / view commands (list and get).
1018
    """
1019
1020
    @classmethod
1021
    def _get_exclude_attributes(cls, args):
1022
        """
1023
        Retrieve a list of exclude attributes for particular command line arguments.
1024
        """
1025
        exclude_attributes = []
1026
1027
        result_included = False
1028
        trigger_instance_included = False
1029
1030
        for attr in args.attr:
1031
            # Note: We perform startswith check so we correctly detected child attribute properties
1032
            # (e.g. result, result.stdout, result.stderr, etc.)
1033
            if attr.startswith('result'):
1034
                result_included = True
1035
1036
            if attr.startswith('trigger_instance'):
1037
                trigger_instance_included = True
1038
1039
        if not result_included:
1040
            exclude_attributes.append('result')
1041
        if not trigger_instance_included:
1042
            exclude_attributes.append('trigger_instance')
1043
1044
        return exclude_attributes
1045
1046
1047
class ActionExecutionListCommand(ActionExecutionReadCommand):
1048
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
1049
                          'end_timestamp']
1050
    attribute_transform_functions = {
1051
        'start_timestamp': format_isodate_for_user_timezone,
1052
        'end_timestamp': format_isodate_for_user_timezone,
1053
        'parameters': format_parameters,
1054
        'status': format_status
1055
    }
1056
1057
    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...
1058
        super(ActionExecutionListCommand, self).__init__(
1059
            resource, 'list', 'Get the list of the 50 most recent %s.' %
1060
            resource.get_plural_display_name().lower(),
1061
            *args, **kwargs)
1062
1063
        self.default_limit = 50
1064
        self.resource_name = resource.get_plural_display_name().lower()
1065
        self.group = self.parser.add_argument_group()
1066
        self.parser.add_argument('-n', '--last', type=int, dest='last',
1067
                                 default=self.default_limit,
1068
                                 help=('List N most recent %s.' % self.resource_name))
1069
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
1070
                                 default='descending',
1071
                                 help=('Sort %s by start timestamp, '
1072
                                       'asc|ascending (earliest first) '
1073
                                       'or desc|descending (latest first)' % self.resource_name))
1074
1075
        # Filter options
1076
        self.group.add_argument('--action', help='Action reference to filter the list.')
1077
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
1078
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
1079
                                                  '\'%s\', \'%s\' or \'%s\''
1080
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
1081
        self.group.add_argument('--trigger_instance',
1082
                                help='Trigger instance id to filter the list.')
1083
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
1084
                                 default=None,
1085
                                 help=('Only return executions with timestamp '
1086
                                       'greater than the one provided. '
1087
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1088
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
1089
                                 default=None,
1090
                                 help=('Only return executions with timestamp '
1091
                                       'lower than the one provided. '
1092
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
1093
        self.parser.add_argument('-l', '--showall', action='store_true',
1094
                                 help='')
1095
1096
        # Display options
1097
        self.parser.add_argument('-a', '--attr', nargs='+',
1098
                                 default=self.display_attributes,
1099
                                 help=('List of attributes to include in the '
1100
                                       'output. "all" will return all '
1101
                                       'attributes.'))
1102
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
1103
                                 default=None,
1104
                                 help=('Set the width of columns in output.'))
1105
1106
    @add_auth_token_to_kwargs_from_cli
1107
    def run(self, args, **kwargs):
1108
        # Filtering options
1109
        if args.action:
1110
            kwargs['action'] = args.action
1111
        if args.status:
1112
            kwargs['status'] = args.status
1113
        if args.trigger_instance:
1114
            kwargs['trigger_instance'] = args.trigger_instance
1115
        if not args.showall:
1116
            # null is the magic string that translates to does not exist.
1117
            kwargs['parent'] = 'null'
1118
        if args.timestamp_gt:
1119
            kwargs['timestamp_gt'] = args.timestamp_gt
1120
        if args.timestamp_lt:
1121
            kwargs['timestamp_lt'] = args.timestamp_lt
1122
        if args.sort_order:
1123
            if args.sort_order in ['asc', 'ascending']:
1124
                kwargs['sort_asc'] = True
1125
            elif args.sort_order in ['desc', 'descending']:
1126
                kwargs['sort_desc'] = True
1127
1128
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
1129
        # since they are not displayed nor used which speeds the common operation substantially.
1130
        exclude_attributes = self._get_exclude_attributes(args=args)
1131
        exclude_attributes = ','.join(exclude_attributes)
1132
        kwargs['exclude_attributes'] = exclude_attributes
1133
1134
        return self.manager.query_with_count(limit=args.last, **kwargs)
1135
1136
    def run_and_print(self, args, **kwargs):
1137
1138
        result, count = self.run(args, **kwargs)
1139
        instances = format_wf_instances(result)
1140
1141
        if args.json or args.yaml:
1142
            self.print_output(reversed(instances), table.MultiColumnTable,
1143
                              attributes=args.attr, widths=args.width,
1144
                              json=args.json,
1145
                              yaml=args.yaml,
1146
                              attribute_transform_functions=self.attribute_transform_functions)
1147
1148
        else:
1149
            # Include elapsed time for running executions
1150
            instances = format_execution_statuses(instances)
1151
            self.print_output(reversed(instances), table.MultiColumnTable,
1152
                              attributes=args.attr, widths=args.width,
1153
                              attribute_transform_functions=self.attribute_transform_functions)
1154
1155
            if args.last and count and count > args.last:
1156
                table.SingleRowTable.note_box(self.resource_name, args.last)
1157
1158
1159
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1160
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1161
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1162
1163
    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...
1164
        super(ActionExecutionGetCommand, self).__init__(
1165
            resource, 'get',
1166
            'Get individual %s.' % resource.get_display_name().lower(),
1167
            *args, **kwargs)
1168
1169
        self.parser.add_argument('id',
1170
                                 help=('ID of the %s.' %
1171
                                       resource.get_display_name().lower()))
1172
1173
        self._add_common_options()
1174
1175
    @add_auth_token_to_kwargs_from_cli
1176
    def run(self, args, **kwargs):
1177
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1178
        # requested by user either via "--attr" flag or by default.
1179
        exclude_attributes = self._get_exclude_attributes(args=args)
1180
        exclude_attributes = ','.join(exclude_attributes)
1181
1182
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1183
1184
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1185
        return execution
1186
1187
    @add_auth_token_to_kwargs_from_cli
1188
    def run_and_print(self, args, **kwargs):
1189
        try:
1190
            execution = self.run(args, **kwargs)
1191
1192
            if not args.json and not args.yaml:
1193
                # Include elapsed time for running executions
1194
                execution = format_execution_status(execution)
1195
        except resource.ResourceNotFoundError:
1196
            self.print_not_found(args.id)
1197
            raise ResourceNotFoundError('Execution with id %s not found.' % (args.id))
1198
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1199
1200
1201
class ActionExecutionCancelCommand(resource.ResourceCommand):
1202
1203
    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...
1204
        super(ActionExecutionCancelCommand, self).__init__(
1205
            resource, 'cancel', 'Cancel %s.' %
1206
            resource.get_plural_display_name().lower(),
1207
            *args, **kwargs)
1208
1209
        self.parser.add_argument('ids',
1210
                                 nargs='+',
1211
                                 help=('IDs of the %ss to cancel.' %
1212
                                       resource.get_display_name().lower()))
1213
1214
    def run(self, args, **kwargs):
1215
        responses = []
1216
        for execution_id in args.ids:
1217
            response = self.manager.delete_by_id(execution_id)
1218
            responses.append([execution_id, response])
1219
1220
        return responses
1221
1222
    @add_auth_token_to_kwargs_from_cli
1223
    def run_and_print(self, args, **kwargs):
1224
        responses = self.run(args, **kwargs)
1225
1226
        for execution_id, response in responses:
1227
            self._print_result(execution_id=execution_id, response=response)
1228
1229
    def _print_result(self, execution_id, response):
1230
        if response and 'faultstring' in response:
1231
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1232
                                   (self.resource.get_display_name().lower(), execution_id))
1233
1234
        elif response:
1235
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1236
                                                   execution_id)
1237
        else:
1238
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1239
                                                        execution_id)
1240
        print(message)
1241
1242
1243
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1244
    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...
1245
1246
        super(ActionExecutionReRunCommand, self).__init__(
1247
            resource, kwargs.pop('name', 're-run'),
1248
            'Re-run a particular action.',
1249
            *args, **kwargs)
1250
1251
        self.parser.add_argument('id', nargs='?',
1252
                                 metavar='id',
1253
                                 help='ID of action execution to re-run ')
1254
        self.parser.add_argument('parameters', nargs='*',
1255
                                 help='List of keyword args, positional args, '
1256
                                      'and optional args for the action.')
1257
        self.parser.add_argument('--tasks', nargs='*',
1258
                                 help='Name of the workflow tasks to re-run.')
1259
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1260
                                 help='Name of the with-items tasks to not reset. This only '
1261
                                      'applies to Mistral workflows. By default, all iterations '
1262
                                      'for with-items tasks is rerun. If no reset, only failed '
1263
                                      ' iterations are rerun.')
1264
        self.parser.add_argument('-a', '--async',
1265
                                 action='store_true', dest='async',
1266
                                 help='Do not wait for action to finish.')
1267
        self.parser.add_argument('-e', '--inherit-env',
1268
                                 action='store_true', dest='inherit_env',
1269
                                 help='Pass all the environment variables '
1270
                                      'which are accessible to the CLI as "env" '
1271
                                      'parameter to the action. Note: Only works '
1272
                                      'with python, local and remote runners.')
1273
        self.parser.add_argument('-h', '--help',
1274
                                 action='store_true', dest='help',
1275
                                 help='Print usage for the given action.')
1276
1277
        self._add_common_options()
1278
1279
    @add_auth_token_to_kwargs_from_cli
1280
    def run(self, args, **kwargs):
1281
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1282
1283
        if not existing_execution:
1284
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1285
                                                 (args.id))
1286
1287
        action_mgr = self.app.client.managers['Action']
1288
        runner_mgr = self.app.client.managers['RunnerType']
1289
        action_exec_mgr = self.app.client.managers['LiveAction']
1290
1291
        action_ref = existing_execution.action['ref']
1292
        action = action_mgr.get_by_ref_or_id(action_ref)
1293
        runner = runner_mgr.get_by_name(action.runner_type)
1294
1295
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1296
                                                                  args=args)
1297
1298
        execution = action_exec_mgr.re_run(execution_id=args.id,
1299
                                           parameters=action_parameters,
1300
                                           tasks=args.tasks,
1301
                                           no_reset=args.no_reset,
1302
                                           **kwargs)
1303
1304
        execution = self._get_execution_result(execution=execution,
1305
                                               action_exec_mgr=action_exec_mgr,
1306
                                               args=args, **kwargs)
1307
1308
        return execution
1309
1310
1311
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1312
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1313
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1314
1315
    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...
1316
        super(ActionExecutionPauseCommand, self).__init__(
1317
            resource, 'pause', 'Pause %s (workflow executions only).' %
1318
            resource.get_plural_display_name().lower(),
1319
            *args, **kwargs)
1320
1321
        self.parser.add_argument('id', nargs='+',
1322
                                 metavar='id',
1323
                                 help='ID of action execution to pause.')
1324
1325
        self._add_common_options()
1326
1327
    @add_auth_token_to_kwargs_from_cli
1328
    def run(self, args, **kwargs):
1329
        return self.manager.pause(args.id)
1330
1331
    @add_auth_token_to_kwargs_from_cli
1332
    def run_and_print(self, args, **kwargs):
1333
        try:
1334
            execution = self.run(args, **kwargs)
1335
1336
            if not args.json and not args.yaml:
1337
                # Include elapsed time for running executions
1338
                execution = format_execution_status(execution)
1339
        except resource.ResourceNotFoundError:
1340
            self.print_not_found(args.id)
1341
            raise ResourceNotFoundError('Execution  with id %s not found.' % (args.id))
1342
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1343
1344
1345
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1346
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1347
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1348
1349
    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...
1350
        super(ActionExecutionResumeCommand, self).__init__(
1351
            resource, 'resume', 'Resume %s (workflow executions only).' %
1352
            resource.get_plural_display_name().lower(),
1353
            *args, **kwargs)
1354
1355
        self.parser.add_argument('id', nargs='+',
1356
                                 metavar='id',
1357
                                 help='ID of action execution to resume.')
1358
1359
        self._add_common_options()
1360
1361
    @add_auth_token_to_kwargs_from_cli
1362
    def run(self, args, **kwargs):
1363
        return self.manager.resume(args.id)
1364
1365
    @add_auth_token_to_kwargs_from_cli
1366
    def run_and_print(self, args, **kwargs):
1367
        try:
1368
            execution = self.run(args, **kwargs)
1369
1370
            if not args.json and not args.yaml:
1371
                # Include elapsed time for running executions
1372
                execution = format_execution_status(execution)
1373
        except resource.ResourceNotFoundError:
1374
            self.print_not_found(args.id)
1375
            raise ResourceNotFoundError('Execution %s not found.' % (args.id))
1376
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1377
1378
1379
class ActionExecutionTailCommand(resource.ResourceCommand):
1380
    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...
1381
        super(ActionExecutionTailCommand, self).__init__(
1382
            resource, kwargs.pop('name', 'tail'),
1383
            'Tail output of a particular execution.',
1384
            *args, **kwargs)
1385
1386
        self.parser.add_argument('id', nargs='?',
1387
                                 metavar='id',
1388
                                 default='last',
1389
                                 help='ID of action execution to tail.')
1390
        self.parser.add_argument('--type', dest='output_type', action='store',
1391
                                 help=('Type of output to tail for. If not provided, '
1392
                                      'defaults to all.'))
1393
        self.parser.add_argument('--include-metadata', dest='include_metadata',
1394
                                 action='store_true',
1395
                                 default=False,
1396
                                 help=('Include metadata (timestamp, output type) with the '
1397
                                       'output.'))
1398
1399
    def run(self, args, **kwargs):
1400
        pass
1401
1402
    @add_auth_token_to_kwargs_from_cli
1403
    def run_and_print(self, args, **kwargs):
1404
        execution_id = args.id
1405
        output_type = getattr(args, 'output_type', None)
1406
        include_metadata = args.include_metadata
1407
1408
        # Special case for id "last"
1409
        if execution_id == 'last':
1410
            executions = self.manager.query(limit=1)
1411
            if executions:
1412
                execution = executions[0]
1413
                execution_id = execution.id
1414
            else:
1415
                print('No executions found in db.')
1416
                return
1417
        else:
1418
            execution = self.manager.get_by_id(execution_id, **kwargs)
1419
1420
        if not execution:
1421
            raise ResourceNotFoundError('Execution  with id %s not found.' % (args.id))
1422
1423
        execution_manager = self.manager
1424
        stream_manager = self.app.client.managers['Stream']
1425
        ActionExecutionTailCommand.tail_execution(execution=execution,
1426
                                                  execution_manager=execution_manager,
1427
                                                  stream_manager=stream_manager,
1428
                                                  output_type=output_type,
1429
                                                  include_metadata=include_metadata,
1430
                                                  **kwargs)
1431
1432
    @classmethod
1433
    def tail_execution(cls, execution_manager, stream_manager, execution, output_type=None,
1434
                       include_metadata=False, **kwargs):
1435
        execution_id = str(execution.id)
1436
        # Note: For non-workflow actions child_execution_id always matches parent_execution_id so
1437
        # we don't need to do any other checks to determine if executions represents a workflow
1438
        # action
1439
        parent_execution_id = execution_id
1440
1441
        # Execution has already finished
1442
        if execution.status in LIVEACTION_COMPLETED_STATES:
1443
            output = execution_manager.get_output(execution_id=execution_id,
1444
                                                  output_type=output_type)
1445
            print(output)
1446
            print('Execution %s has completed (status=%s).' % (execution_id, execution.status))
1447
            return
1448
1449
        events = ['st2.execution__update', 'st2.execution.output__create']
1450
1451
        for event in stream_manager.listen(events, **kwargs):
1452
            status = event.get('status', None)
1453
            is_execution_event = status is not None
1454
1455
            # NOTE: Right now only a single level deep / nested workflows are supported
1456
            if is_execution_event:
1457
                context = cls.get_normalized_context_execution_task_event(event=event)
1458
                task_execution_id = context['execution_id']
1459
                task_name = context['task_name']
1460
                task_parent_execution_id = context['parent_execution_id']
1461
                is_child_execution = (task_parent_execution_id == parent_execution_id)
1462
1463
                if is_child_execution:
1464
                    if status == LIVEACTION_STATUS_RUNNING:
1465
                        print('Child execution (task=%s) %s has started.' % (task_name,
1466
                                                                             task_execution_id))
1467
                        print('')
1468
                        continue
1469
                    elif status in LIVEACTION_COMPLETED_STATES:
1470
                        print('')
1471
                        print('Child execution (task=%s) %s has finished (status=%s).' % (task_name,
1472
                              task_execution_id, status))
1473
                        continue
1474
                    else:
1475
                        # We don't care about other child events so we simply skip then
1476
                        continue
1477
                else:
1478
                    if status in LIVEACTION_COMPLETED_STATES:
1479
                        # Bail out once parent execution has finished
1480
                        print('')
1481
                        print('Execution %s has completed (status=%s).' % (execution_id, status))
1482
                        break
1483
                    else:
1484
                        # We don't care about other execution events
1485
                        continue
1486
1487
            # Filter on output_type if provided
1488
            event_output_type = event.get('output_type', None)
1489
            if output_type != 'all' and output_type and (event_output_type != output_type):
1490
                continue
1491
1492
            if include_metadata:
1493
                sys.stdout.write('[%s][%s] %s' % (event['timestamp'], event['output_type'],
1494
                                                  event['data']))
1495
            else:
1496
                sys.stdout.write(event['data'])
1497
1498
    @classmethod
1499
    def get_normalized_context_execution_task_event(cls, event):
1500
        """
1501
        Return a dictionary with normalized context attributes for Action-Chain and Mistral
1502
        workflows.
1503
        """
1504
        context = event.get('context', {})
1505
1506
        result = {
1507
            'parent_execution_id': None,
1508
            'execution_id': None,
1509
            'task_name': None
1510
        }
1511
1512
        if 'mistral' in context:
1513
            # Mistral workflow
1514
            result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None)
1515
            result['execution_id'] = event['id']
1516
            result['task_name'] = context.get('mistral', {}).get('task_name', 'unknown')
1517
        else:
1518
            # Action chain workflow
1519
            result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None)
1520
            result['execution_id'] = event['id']
1521
            result['task_name'] = context.get('chain', {}).get('name', 'unknown')
1522
1523
        return result
1524