GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — kale/submit-debug-info ( 1f660b )
by
unknown
05:52
created

st2client.commands.format_execution_status()   B

Complexity

Conditions 6

Size

Total Lines 26

Duplication

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