Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2client/st2client/commands/action.py (2 issues)

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

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...
1228
        super(ActionExecutionCancelCommand, self).__init__(
1229
            resource, 'cancel', 'Cancel %s.' %
1230
            resource.get_plural_display_name().lower(),
1231
            *args, **kwargs)
1232
1233
        self.parser.add_argument('ids',
1234
                                 nargs='+',
1235
                                 help=('IDs of the %ss to cancel.' %
1236
                                       resource.get_display_name().lower()))
1237
1238
    def run(self, args, **kwargs):
1239
        responses = []
1240
        for execution_id in args.ids:
1241
            response = self.manager.delete_by_id(execution_id)
1242
            responses.append([execution_id, response])
1243
1244
        return responses
1245
1246
    @add_auth_token_to_kwargs_from_cli
1247
    def run_and_print(self, args, **kwargs):
1248
        responses = self.run(args, **kwargs)
1249
1250
        for execution_id, response in responses:
1251
            self._print_result(execution_id=execution_id, response=response)
1252
1253
    def _print_result(self, execution_id, response):
1254
        if response and 'faultstring' in response:
1255
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1256
                                   (self.resource.get_display_name().lower(), execution_id))
1257
1258
        elif response:
1259
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1260
                                                   execution_id)
1261
        else:
1262
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1263
                                                        execution_id)
1264
        print(message)
1265
1266
1267
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1268
    def __init__(self, resource, *args, **kwargs):
1269
1270
        super(ActionExecutionReRunCommand, self).__init__(
1271
            resource, kwargs.pop('name', 're-run'),
1272
            'Re-run a particular action.',
1273
            *args, **kwargs)
1274
1275
        self.parser.add_argument('id', nargs='?',
1276
                                 metavar='id',
1277
                                 help='ID of action execution to re-run ')
1278
        self.parser.add_argument('parameters', nargs='*',
1279
                                 help='List of keyword args, positional args, '
1280
                                      'and optional args for the action.')
1281
        self.parser.add_argument('--tasks', nargs='*',
1282
                                 help='Name of the workflow tasks to re-run.')
1283
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1284
                                 help='Name of the with-items tasks to not reset. This only '
1285
                                      'applies to Mistral workflows. By default, all iterations '
1286
                                      'for with-items tasks is rerun. If no reset, only failed '
1287
                                      ' iterations are rerun.')
1288
        self.parser.add_argument('-a', '--async',
1289
                                 action='store_true', dest='action_async',
1290
                                 help='Do not wait for action to finish.')
1291
        self.parser.add_argument('-e', '--inherit-env',
1292
                                 action='store_true', dest='inherit_env',
1293
                                 help='Pass all the environment variables '
1294
                                      'which are accessible to the CLI as "env" '
1295
                                      'parameter to the action. Note: Only works '
1296
                                      'with python, local and remote runners.')
1297
        self.parser.add_argument('-h', '--help',
1298
                                 action='store_true', dest='help',
1299
                                 help='Print usage for the given action.')
1300
1301
        self._add_common_options()
1302
1303
    @add_auth_token_to_kwargs_from_cli
1304
    def run(self, args, **kwargs):
1305
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1306
1307
        if not existing_execution:
1308
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1309
                                                 (args.id))
1310
1311
        action_mgr = self.app.client.managers['Action']
1312
        runner_mgr = self.app.client.managers['RunnerType']
1313
        action_exec_mgr = self.app.client.managers['LiveAction']
1314
1315
        action_ref = existing_execution.action['ref']
1316
        action = action_mgr.get_by_ref_or_id(action_ref)
1317
        runner = runner_mgr.get_by_name(action.runner_type)
1318
1319
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1320
                                                                  args=args)
1321
1322
        execution = action_exec_mgr.re_run(execution_id=args.id,
1323
                                           parameters=action_parameters,
1324
                                           tasks=args.tasks,
1325
                                           no_reset=args.no_reset,
1326
                                           **kwargs)
1327
1328
        execution = self._get_execution_result(execution=execution,
1329
                                               action_exec_mgr=action_exec_mgr,
1330
                                               args=args, **kwargs)
1331
1332
        return execution
1333
1334
1335
class ActionExecutionPauseCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1336
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1337
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1338
1339
    def __init__(self, resource, *args, **kwargs):
1340
        super(ActionExecutionPauseCommand, self).__init__(
1341
            resource, 'pause', 'Pause %s (workflow executions only).' %
1342
            resource.get_plural_display_name().lower(),
1343
            *args, **kwargs)
1344
1345
        self.parser.add_argument('ids',
1346
                                 nargs='+',
1347
                                 help='ID of action execution to pause.')
1348
1349
        self._add_common_options()
1350
1351
    @add_auth_token_to_kwargs_from_cli
1352
    def run(self, args, **kwargs):
1353
        responses = []
1354
        for execution_id in args.ids:
1355
            try:
1356
                response = self.manager.pause(execution_id)
1357
                responses.append([execution_id, response])
1358
            except resource.ResourceNotFoundError:
1359
                self.print_not_found(args.ids)
1360
                raise ResourceNotFoundError('Execution with id %s not found.' % (execution_id))
1361
1362
        return responses
1363
1364
    @add_auth_token_to_kwargs_from_cli
1365
    def run_and_print(self, args, **kwargs):
1366
        responses = self.run(args, **kwargs)
1367
1368
        for execution_id, response in responses:
1369
            self._print_result(args, execution_id, response, **kwargs)
1370
1371
    def _print_result(self, args, execution_id, execution, **kwargs):
1372
        if not args.json and not args.yaml:
1373
            # Include elapsed time for running executions
1374
            execution = format_execution_status(execution)
1375
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1376
1377
1378
class ActionExecutionResumeCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1379
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1380
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1381
1382
    def __init__(self, resource, *args, **kwargs):
1383
        super(ActionExecutionResumeCommand, self).__init__(
1384
            resource, 'resume', 'Resume %s (workflow executions only).' %
1385
            resource.get_plural_display_name().lower(),
1386
            *args, **kwargs)
1387
1388
        self.parser.add_argument('ids',
1389
                                 nargs='+',
1390
                                 help='ID of action execution to resume.')
1391
1392
        self._add_common_options()
1393
1394
    @add_auth_token_to_kwargs_from_cli
1395
    def run(self, args, **kwargs):
1396
        responses = []
1397
        for execution_id in args.ids:
1398
            try:
1399
                response = self.manager.resume(execution_id)
1400
                responses.append([execution_id, response])
1401
            except resource.ResourceNotFoundError:
1402
                self.print_not_found(execution_id)
1403
                raise ResourceNotFoundError('Execution with id %s not found.' % (execution_id))
1404
1405
        return responses
1406
1407
    @add_auth_token_to_kwargs_from_cli
1408
    def run_and_print(self, args, **kwargs):
1409
        responses = self.run(args, **kwargs)
1410
1411
        for execution_id, response in responses:
1412
            self._print_result(args, response, **kwargs)
1413
1414
    def _print_result(self, args, execution, **kwargs):
1415
        if not args.json and not args.yaml:
1416
            # Include elapsed time for running executions
1417
            execution = format_execution_status(execution)
1418
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1419
1420
1421
class ActionExecutionTailCommand(resource.ResourceCommand):
1422
    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 34).

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...
1423
        super(ActionExecutionTailCommand, self).__init__(
1424
            resource, kwargs.pop('name', 'tail'),
1425
            'Tail output of a particular execution.',
1426
            *args, **kwargs)
1427
1428
        self.parser.add_argument('id', nargs='?',
1429
                                 metavar='id',
1430
                                 default='last',
1431
                                 help='ID of action execution to tail.')
1432
        self.parser.add_argument('--type', dest='output_type', action='store',
1433
                                 help=('Type of output to tail for. If not provided, '
1434
                                       'defaults to all.'))
1435
        self.parser.add_argument('--include-metadata', dest='include_metadata',
1436
                                 action='store_true',
1437
                                 default=False,
1438
                                 help=('Include metadata (timestamp, output type) with the '
1439
                                       'output.'))
1440
1441
    def run(self, args, **kwargs):
1442
        pass
1443
1444
    @add_auth_token_to_kwargs_from_cli
1445
    def run_and_print(self, args, **kwargs):
1446
        execution_id = args.id
1447
        output_type = getattr(args, 'output_type', None)
1448
        include_metadata = args.include_metadata
1449
1450
        # Special case for id "last"
1451
        if execution_id == 'last':
1452
            executions = self.manager.query(limit=1)
1453
            if executions:
1454
                execution = executions[0]
1455
                execution_id = execution.id
1456
            else:
1457
                print('No executions found in db.')
1458
                return
1459
        else:
1460
            execution = self.manager.get_by_id(execution_id, **kwargs)
1461
1462
        if not execution:
1463
            raise ResourceNotFoundError('Execution  with id %s not found.' % (args.id))
1464
1465
        execution_manager = self.manager
1466
        stream_manager = self.app.client.managers['Stream']
1467
        ActionExecutionTailCommand.tail_execution(execution=execution,
1468
                                                  execution_manager=execution_manager,
1469
                                                  stream_manager=stream_manager,
1470
                                                  output_type=output_type,
1471
                                                  include_metadata=include_metadata,
1472
                                                  **kwargs)
1473
1474
    @classmethod
1475
    def tail_execution(cls, execution_manager, stream_manager, execution, output_type=None,
1476
                       include_metadata=False, **kwargs):
1477
        execution_id = str(execution.id)
1478
1479
        # Note: For non-workflow actions child_execution_id always matches parent_execution_id so
1480
        # we don't need to do any other checks to determine if executions represents a workflow
1481
        # action
1482
        parent_execution_id = execution_id  # noqa
1483
1484
        # Execution has already finished
1485
        if execution.status in LIVEACTION_COMPLETED_STATES:
1486
            output = execution_manager.get_output(execution_id=execution_id,
1487
                                                  output_type=output_type)
1488
            print(output)
1489
            print('Execution %s has completed (status=%s).' % (execution_id, execution.status))
1490
            return
1491
1492
        events = ['st2.execution__update', 'st2.execution.output__create']
1493
1494
        for event in stream_manager.listen(events, **kwargs):
1495
            status = event.get('status', None)
1496
            is_execution_event = status is not None
1497
1498
            # NOTE: Right now only a single level deep / nested workflows are supported
1499
            if is_execution_event:
1500
                context = cls.get_normalized_context_execution_task_event(event=event)
1501
                task_execution_id = context['execution_id']
1502
                task_name = context['task_name']
1503
                task_parent_execution_id = context['parent_execution_id']
1504
1505
                # An execution is considered a child execution if it has parent execution id
1506
                is_child_execution = bool(task_parent_execution_id)
1507
1508
                if is_child_execution:
1509
                    if status == LIVEACTION_STATUS_RUNNING:
1510
                        print('Child execution (task=%s) %s has started.' % (task_name,
1511
                                                                             task_execution_id))
1512
                        print('')
1513
                        continue
1514
                    elif status in LIVEACTION_COMPLETED_STATES:
1515
                        print('')
1516
                        print('Child execution (task=%s) %s has finished (status=%s).' %
1517
                              (task_name, task_execution_id, status))
1518
                        continue
1519
                    else:
1520
                        # We don't care about other child events so we simply skip then
1521
                        continue
1522
                else:
1523
                    if status == LIVEACTION_STATUS_RUNNING:
1524
                        print('Execution %s has started.' % (execution_id))
1525
                        print('')
1526
                        continue
1527
                    elif status in LIVEACTION_COMPLETED_STATES:
1528
                        # Bail out once parent execution has finished
1529
                        print('')
1530
                        print('Execution %s has completed (status=%s).' % (execution_id, status))
1531
                        break
1532
                    else:
1533
                        # We don't care about other execution events
1534
                        continue
1535
1536
            # Filter on output_type if provided
1537
            event_output_type = event.get('output_type', None)
1538
            if output_type != 'all' and output_type and (event_output_type != output_type):
1539
                continue
1540
1541
            if include_metadata:
1542
                sys.stdout.write('[%s][%s] %s' % (event['timestamp'], event['output_type'],
1543
                                                  event['data']))
1544
            else:
1545
                sys.stdout.write(event['data'])
1546
1547
    @classmethod
1548
    def get_normalized_context_execution_task_event(cls, event):
1549
        """
1550
        Return a dictionary with normalized context attributes for Action-Chain and Mistral
1551
        workflows.
1552
        """
1553
        context = event.get('context', {})
1554
1555
        result = {
1556
            'parent_execution_id': None,
1557
            'execution_id': None,
1558
            'task_name': None
1559
        }
1560
1561
        if 'mistral' in context:
1562
            # Mistral workflow
1563
            result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None)
1564
            result['execution_id'] = event['id']
1565
            result['task_name'] = context.get('mistral', {}).get('task_name', 'unknown')
1566
        else:
1567
            # Action chain workflow
1568
            result['parent_execution_id'] = context.get('parent', {}).get('execution_id', None)
1569
            result['execution_id'] = event['id']
1570
            result['task_name'] = context.get('chain', {}).get('name', 'unknown')
1571
1572
        return result
1573