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.
Test Failed
Push — develop-v1.6.0 ( 9d5181...7efb31 )
by
unknown
04:49
created

ActionRunCommandMixin._print_help()   F

Complexity

Conditions 12

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
dl 0
loc 50
rs 2.6548
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like ActionRunCommandMixin._print_help() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import ast
18
import copy
19
import json
20
import logging
21
import textwrap
22
import calendar
23
import time
24
import six
25
import sys
26
27
from os.path import join as pjoin
28
29
from st2client import models
30
from st2client.commands import resource
31
from st2client.commands.resource import add_auth_token_to_kwargs_from_cli
32
from st2client.exceptions.operations import OperationFailureException
33
from st2client.formatters import table
34
from st2client.formatters import execution as execution_formatter
35
from st2client.utils import jsutil
36
from st2client.utils.date import format_isodate_for_user_timezone
37
from st2client.utils.date import parse as parse_isotime
38
from st2client.utils.color import format_status
39
40
LOG = logging.getLogger(__name__)
41
42
LIVEACTION_STATUS_REQUESTED = 'requested'
43
LIVEACTION_STATUS_SCHEDULED = 'scheduled'
44
LIVEACTION_STATUS_DELAYED = 'delayed'
45
LIVEACTION_STATUS_RUNNING = 'running'
46
LIVEACTION_STATUS_SUCCEEDED = 'succeeded'
47
LIVEACTION_STATUS_FAILED = 'failed'
48
LIVEACTION_STATUS_TIMED_OUT = 'timeout'
49
LIVEACTION_STATUS_ABANDONED = 'abandoned'
50
LIVEACTION_STATUS_CANCELING = 'canceling'
51
LIVEACTION_STATUS_CANCELED = 'canceled'
52
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_for_user_timezone,
239
        'end_timestamp': format_isodate_for_user_timezone,
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
                              yaml=args.yaml,
399
                              attribute_transform_functions=self.attribute_transform_functions)
400
401
    def _get_execution_result(self, execution, action_exec_mgr, args, **kwargs):
402
        pending_statuses = [
403
            LIVEACTION_STATUS_REQUESTED,
404
            LIVEACTION_STATUS_SCHEDULED,
405
            LIVEACTION_STATUS_RUNNING,
406
            LIVEACTION_STATUS_CANCELING
407
        ]
408
409
        if not args.async:
410
            while execution.status in pending_statuses:
411
                time.sleep(self.poll_interval)
412
                if not args.json and not args.yaml:
413
                    sys.stdout.write('.')
414
                    sys.stdout.flush()
415
                execution = action_exec_mgr.get_by_id(execution.id, **kwargs)
416
417
            sys.stdout.write('\n')
418
419
            if execution.status == LIVEACTION_STATUS_CANCELED:
420
                return execution
421
422
        return execution
423
424
    def _get_top_level_error(self, live_action):
425
        """
426
        Retrieve a top level workflow error.
427
428
        :return: (error, traceback)
429
        """
430
        if isinstance(live_action.result, dict):
431
            error = live_action.result.get('error', None)
432
            traceback = live_action.result.get('traceback', None)
433
        else:
434
            error = "See result"
435
            traceback = "See result"
436
437
        return error, traceback
438
439
    def _get_task_error(self, task):
440
        """
441
        Retrieve error message from the provided task.
442
443
        :return: (error, traceback)
444
        """
445
        if not task:
446
            return None, None
447
448
        result = task['result']
449
450
        if isinstance(result, dict):
451
            stderr = result.get('stderr', None)
452
            error = result.get('error', None)
453
            traceback = result.get('traceback', None)
454
            error = error if error else stderr
455
        else:
456
            stderr = None
457
            error = None
458
            traceback = None
459
460
        return error, traceback
461
462
    def _get_action_parameters_from_args(self, action, runner, args):
463
        """
464
        Build a dictionary with parameters which will be passed to the action by
465
        parsing parameters passed to the CLI.
466
467
        :param args: CLI argument.
468
        :type args: ``object``
469
470
        :rtype: ``dict``
471
        """
472
        action_ref_or_id = action.ref
473
474
        def read_file(file_path):
475
            if not os.path.exists(file_path):
476
                raise ValueError('File "%s" doesn\'t exist' % (file_path))
477
478
            if not os.path.isfile(file_path):
479
                raise ValueError('"%s" is not a file' % (file_path))
480
481
            with open(file_path, 'rb') as fp:
482
                content = fp.read()
483
484
            return content
485
486
        def transform_object(value):
487
            # Also support simple key1=val1,key2=val2 syntax
488
            if value.startswith('{'):
489
                # Assume it's JSON
490
                result = value = json.loads(value)
491
            else:
492
                pairs = value.split(',')
493
494
                result = {}
495
                for pair in pairs:
496
                    split = pair.split('=', 1)
497
498
                    if len(split) != 2:
499
                        continue
500
501
                    key, value = split
502
                    result[key] = value
503
            return result
504
505
        transformer = {
506
            'array': (lambda cs_x: [v.strip() for v in cs_x.split(',')]),
507
            'boolean': (lambda x: ast.literal_eval(x.capitalize())),
508
            'integer': int,
509
            'number': float,
510
            'object': transform_object,
511
            'string': str
512
        }
513
514
        def normalize(name, value):
515
            if name in runner.runner_parameters:
516
                param = runner.runner_parameters[name]
517
                if 'type' in param and param['type'] in transformer:
518
                    return transformer[param['type']](value)
519
520
            if name in action.parameters:
521
                param = action.parameters[name]
522
                if 'type' in param and param['type'] in transformer:
523
                    return transformer[param['type']](value)
524
            return value
525
526
        result = {}
527
528
        if not args.parameters:
529
            return result
530
531
        for idx in range(len(args.parameters)):
532
            arg = args.parameters[idx]
533
            if '=' in arg:
534
                k, v = arg.split('=', 1)
535
536
                # Attribute for files are prefixed with "@"
537
                if k.startswith('@'):
538
                    k = k[1:]
539
                    is_file = True
540
                else:
541
                    is_file = False
542
543
                try:
544
                    if is_file:
545
                        # Files are handled a bit differently since we ship the content
546
                        # over the wire
547
                        file_path = os.path.normpath(pjoin(os.getcwd(), v))
548
                        file_name = os.path.basename(file_path)
549
                        content = read_file(file_path=file_path)
550
551
                        if action_ref_or_id == 'core.http':
552
                            # Special case for http runner
553
                            result['_file_name'] = file_name
554
                            result['file_content'] = content
555
                        else:
556
                            result[k] = content
557
                    else:
558
                        result[k] = normalize(k, v)
559
                except Exception as e:
560
                    # TODO: Move transformers in a separate module and handle
561
                    # exceptions there
562
                    if 'malformed string' in str(e):
563
                        message = ('Invalid value for boolean parameter. '
564
                                   'Valid values are: true, false')
565
                        raise ValueError(message)
566
                    else:
567
                        raise e
568
            else:
569
                result['cmd'] = ' '.join(args.parameters[idx:])
570
                break
571
572
        # Special case for http runner
573
        if 'file_content' in result:
574
            if 'method' not in result:
575
                # Default to POST if a method is not provided
576
                result['method'] = 'POST'
577
578
            if 'file_name' not in result:
579
                # File name not provided, use default file name
580
                result['file_name'] = result['_file_name']
581
582
            del result['_file_name']
583
584
        if args.inherit_env:
585
            result['env'] = self._get_inherited_env_vars()
586
587
        return result
588
589
    @add_auth_token_to_kwargs_from_cli
590
    def _print_help(self, args, **kwargs):
591
        # Print appropriate help message if the help option is given.
592
        action_mgr = self.app.client.managers['Action']
593
        action_exec_mgr = self.app.client.managers['LiveAction']
594
595
        if args.help:
596
            action_ref_or_id = getattr(args, 'ref_or_id', None)
597
            action_exec_id = getattr(args, 'id', None)
598
599
            if action_exec_id and not action_ref_or_id:
600
                action_exec = action_exec_mgr.get_by_id(action_exec_id, **kwargs)
601
                args.ref_or_id = action_exec.action
602
603
            if action_ref_or_id:
604
                try:
605
                    action = action_mgr.get_by_ref_or_id(args.ref_or_id, **kwargs)
606
                    if not action:
607
                        raise resource.ResourceNotFoundError('Action %s not found', args.ref_or_id)
608
                    runner_mgr = self.app.client.managers['RunnerType']
609
                    runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
610
                    parameters, required, optional, _ = self._get_params_types(runner,
611
                                                                               action)
612
                    print('')
613
                    print(textwrap.fill(action.description))
614
                    print('')
615
                    if required:
616
                        required = self._sort_parameters(parameters=parameters,
617
                                                         names=required)
618
619
                        print('Required Parameters:')
620
                        [self._print_param(name, parameters.get(name))
621
                            for name in required]
622
                    if optional:
623
                        optional = self._sort_parameters(parameters=parameters,
624
                                                         names=optional)
625
626
                        print('Optional Parameters:')
627
                        [self._print_param(name, parameters.get(name))
628
                            for name in optional]
629
                except resource.ResourceNotFoundError:
630
                    print(('Action "%s" is not found. ' % args.ref_or_id) +
631
                          'Do "st2 action list" to see list of available actions.')
632
                except Exception as e:
633
                    print('ERROR: Unable to print help for action "%s". %s' %
634
                          (args.ref_or_id, e))
635
            else:
636
                self.parser.print_help()
637
            return True
638
        return False
639
640
    @staticmethod
641
    def _print_param(name, schema):
642
        if not schema:
643
            raise ValueError('Missing schema for parameter "%s"' % (name))
644
645
        wrapper = textwrap.TextWrapper(width=78)
646
        wrapper.initial_indent = ' ' * 4
647
        wrapper.subsequent_indent = wrapper.initial_indent
648
        print(wrapper.fill(name))
649
        wrapper.initial_indent = ' ' * 8
650
        wrapper.subsequent_indent = wrapper.initial_indent
651
        if 'description' in schema and schema['description']:
652
            print(wrapper.fill(schema['description']))
653
        if 'type' in schema and schema['type']:
654
            print(wrapper.fill('Type: %s' % schema['type']))
655
        if 'enum' in schema and schema['enum']:
656
            print(wrapper.fill('Enum: %s' % ', '.join(schema['enum'])))
657
        if 'default' in schema and schema['default'] is not None:
658
            print(wrapper.fill('Default: %s' % schema['default']))
659
        print('')
660
661
    @staticmethod
662
    def _get_params_types(runner, action):
663
        runner_params = runner.runner_parameters
664
        action_params = action.parameters
665
        parameters = copy.copy(runner_params)
666
        parameters.update(copy.copy(action_params))
667
        required = set([k for k, v in six.iteritems(parameters) if v.get('required')])
668
669
        def is_immutable(runner_param_meta, action_param_meta):
670
            # If runner sets a param as immutable, action cannot override that.
671
            if runner_param_meta.get('immutable', False):
672
                return True
673
            else:
674
                return action_param_meta.get('immutable', False)
675
676
        immutable = set()
677
        for param in parameters.keys():
678
            if is_immutable(runner_params.get(param, {}),
679
                            action_params.get(param, {})):
680
                immutable.add(param)
681
682
        required = required - immutable
683
        optional = set(parameters.keys()) - required - immutable
684
685
        return parameters, required, optional, immutable
686
687
    def _format_child_instances(self, children, parent_id):
688
        '''
689
        The goal of this method is to add an indent at every level. This way the
690
        WF is represented as a tree structure while in a list. For the right visuals
691
        representation the list must be a DF traversal else the idents will end up
692
        looking strange.
693
        '''
694
        # apply basic WF formating first.
695
        children = format_wf_instances(children)
696
        # setup a depth lookup table
697
        depth = {parent_id: 0}
698
        result = []
699
        # main loop that indents each entry correctly
700
        for child in children:
701
            # make sure child.parent is in depth and while at it compute the
702
            # right depth for indentation purposes.
703
            if child.parent not in depth:
704
                parent = None
705
                for instance in children:
706
                    if WF_PREFIX in instance.id:
707
                        instance_id = instance.id[instance.id.index(WF_PREFIX) + len(WF_PREFIX):]
708
                    else:
709
                        instance_id = instance.id
710
                    if instance_id == child.parent:
711
                        parent = instance
712
                if parent and parent.parent and parent.parent in depth:
713
                    depth[child.parent] = depth[parent.parent] + 1
714
                else:
715
                    depth[child.parent] = 0
716
            # now ident for the right visuals
717
            child.id = INDENT_CHAR * depth[child.parent] + child.id
718
            result.append(self._format_for_common_representation(child))
719
        return result
720
721
    def _format_for_common_representation(self, task):
722
        '''
723
        Formats a task for common representation between mistral and action-chain.
724
        '''
725
        # This really needs to be better handled on the back-end but that would be a bigger
726
        # change so handling in cli.
727
        context = getattr(task, 'context', None)
728
        if context and 'chain' in context:
729
            task_name_key = 'context.chain.name'
730
        elif context and 'mistral' in context:
731
            task_name_key = 'context.mistral.task_name'
732
        # Use LiveAction as the object so that the formatter lookup does not change.
733
        # AKA HACK!
734
        return models.action.LiveAction(**{
735
            'id': task.id,
736
            'status': task.status,
737
            'task': jsutil.get_value(vars(task), task_name_key),
738
            'action': task.action.get('ref', None),
739
            'start_timestamp': task.start_timestamp,
740
            'end_timestamp': getattr(task, 'end_timestamp', None)
741
        })
742
743
    def _sort_parameters(self, parameters, names):
744
        """
745
        Sort a provided list of action parameters.
746
747
        :type parameters: ``list``
748
        :type names: ``list`` or ``set``
749
        """
750
        sorted_parameters = sorted(names, key=lambda name:
751
                                   self._get_parameter_sort_value(
752
                                       parameters=parameters,
753
                                       name=name))
754
755
        return sorted_parameters
756
757
    def _get_parameter_sort_value(self, parameters, name):
758
        """
759
        Return a value which determines sort order for a particular parameter.
760
761
        By default, parameters are sorted using "position" parameter attribute.
762
        If this attribute is not available, parameter is sorted based on the
763
        name.
764
        """
765
        parameter = parameters.get(name, None)
766
767
        if not parameter:
768
            return None
769
770
        sort_value = parameter.get('position', name)
771
        return sort_value
772
773
    def _get_inherited_env_vars(self):
774
        env_vars = os.environ.copy()
775
776
        for var_name in ENV_VARS_BLACKLIST:
777
            if var_name.lower() in env_vars:
778
                del env_vars[var_name.lower()]
779
            if var_name.upper() in env_vars:
780
                del env_vars[var_name.upper()]
781
782
        return env_vars
783
784
785
class ActionRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
786
    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...
787
788
        super(ActionRunCommand, self).__init__(
789
            resource, kwargs.pop('name', 'execute'),
790
            'A command to invoke an action manually.',
791
            *args, **kwargs)
792
793
        self.parser.add_argument('ref_or_id', nargs='?',
794
                                 metavar='ref-or-id',
795
                                 help='Action reference (pack.action_name) ' +
796
                                 'or ID of the action.')
797
        self.parser.add_argument('parameters', nargs='*',
798
                                 help='List of keyword args, positional args, '
799
                                      'and optional args for the action.')
800
801
        self.parser.add_argument('-h', '--help',
802
                                 action='store_true', dest='help',
803
                                 help='Print usage for the given action.')
804
805
        self._add_common_options()
806
807
        if self.name in ['run', 'execute']:
808
            self.parser.add_argument('--trace-tag', '--trace_tag',
809
                                     help='A trace tag string to track execution later.',
810
                                     dest='trace_tag', required=False)
811
            self.parser.add_argument('--trace-id',
812
                                     help='Existing trace id for this execution.',
813
                                     dest='trace_id', required=False)
814
            self.parser.add_argument('-a', '--async',
815
                                     action='store_true', dest='async',
816
                                     help='Do not wait for action to finish.')
817
            self.parser.add_argument('-e', '--inherit-env',
818
                                     action='store_true', dest='inherit_env',
819
                                     help='Pass all the environment variables '
820
                                          'which are accessible to the CLI as "env" '
821
                                          'parameter to the action. Note: Only works '
822
                                          'with python, local and remote runners.')
823
            self.parser.add_argument('-u', '--user', type=str, default=None,
824
                                           help='User under which to run the action (admins only).')
825
826
        if self.name == 'run':
827
            self.parser.set_defaults(async=False)
828
        else:
829
            self.parser.set_defaults(async=True)
830
831
    @add_auth_token_to_kwargs_from_cli
832
    def run(self, args, **kwargs):
833
        if not args.ref_or_id:
834
            self.parser.error('Missing action reference or id')
835
836
        action = self.get_resource(args.ref_or_id, **kwargs)
837
        if not action:
838
            raise resource.ResourceNotFoundError('Action "%s" cannot be found.'
839
                                                 % (args.ref_or_id))
840
841
        runner_mgr = self.app.client.managers['RunnerType']
842
        runner = runner_mgr.get_by_name(action.runner_type, **kwargs)
843
        if not runner:
844
            raise resource.ResourceNotFoundError('Runner type "%s" for action "%s" cannot be found.'
845
                                                 % (action.runner_type, action.name))
846
847
        action_ref = '.'.join([action.pack, action.name])
848
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
849
                                                                  args=args)
850
851
        execution = models.LiveAction()
852
        execution.action = action_ref
853
        execution.parameters = action_parameters
854
        execution.user = args.user
855
856
        if not args.trace_id and args.trace_tag:
857
            execution.context = {'trace_context': {'trace_tag': args.trace_tag}}
858
859
        if args.trace_id:
860
            execution.context = {'trace_context': {'id_': args.trace_id}}
861
862
        action_exec_mgr = self.app.client.managers['LiveAction']
863
864
        execution = action_exec_mgr.create(execution, **kwargs)
865
        execution = self._get_execution_result(execution=execution,
866
                                               action_exec_mgr=action_exec_mgr,
867
                                               args=args, **kwargs)
868
        return execution
869
870
871
class ActionExecutionBranch(resource.ResourceBranch):
872
873
    def __init__(self, description, app, subparsers, parent_parser=None):
874
        super(ActionExecutionBranch, self).__init__(
875
            models.LiveAction, description, app, subparsers,
876
            parent_parser=parent_parser, read_only=True,
877
            commands={'list': ActionExecutionListCommand,
878
                      'get': ActionExecutionGetCommand})
879
880
        # Register extended commands
881
        self.commands['re-run'] = ActionExecutionReRunCommand(self.resource, self.app,
882
                                                              self.subparsers, add_help=False)
883
        self.commands['cancel'] = ActionExecutionCancelCommand(self.resource, self.app,
884
                                                               self.subparsers, add_help=False)
885
886
887
POSSIBLE_ACTION_STATUS_VALUES = ('succeeded', 'running', 'scheduled', 'failed', 'canceled')
888
889
890
class ActionExecutionReadCommand(resource.ResourceCommand):
891
    """
892
    Base class for read / view commands (list and get).
893
    """
894
895
    def _get_exclude_attributes(self, args):
896
        """
897
        Retrieve a list of exclude attributes for particular command line arguments.
898
        """
899
        exclude_attributes = []
900
901
        if 'result' not in args.attr:
902
            exclude_attributes.append('result')
903
        if 'trigger_instance' not in args.attr:
904
            exclude_attributes.append('trigger_instance')
905
906
        return exclude_attributes
907
908
909
class ActionExecutionListCommand(ActionExecutionReadCommand):
910
    display_attributes = ['id', 'action.ref', 'context.user', 'status', 'start_timestamp',
911
                          'end_timestamp']
912
    attribute_transform_functions = {
913
        'start_timestamp': format_isodate_for_user_timezone,
914
        'end_timestamp': format_isodate_for_user_timezone,
915
        'parameters': format_parameters,
916
        'status': format_status
917
    }
918
919
    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...
920
        super(ActionExecutionListCommand, self).__init__(
921
            resource, 'list', 'Get the list of the 50 most recent %s.' %
922
            resource.get_plural_display_name().lower(),
923
            *args, **kwargs)
924
925
        self.group = self.parser.add_argument_group()
926
        self.parser.add_argument('-n', '--last', type=int, dest='last',
927
                                 default=50,
928
                                 help=('List N most recent %s.' %
929
                                       resource.get_plural_display_name().lower()))
930
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
931
                                 default='descending',
932
                                 help=('Sort %s by start timestamp, '
933
                                       'asc|ascending (earliest first) '
934
                                       'or desc|descending (latest first)' %
935
                                       resource.get_plural_display_name().lower()))
936
937
        # Filter options
938
        self.group.add_argument('--action', help='Action reference to filter the list.')
939
        self.group.add_argument('--status', help=('Only return executions with the provided status.'
940
                                                  ' Possible values are \'%s\', \'%s\', \'%s\','
941
                                                  '\'%s\' or \'%s\''
942
                                                  '.' % POSSIBLE_ACTION_STATUS_VALUES))
943
        self.group.add_argument('--trigger_instance',
944
                                help='Trigger instance id to filter the list.')
945
        self.parser.add_argument('-tg', '--timestamp-gt', type=str, dest='timestamp_gt',
946
                                 default=None,
947
                                 help=('Only return executions with timestamp '
948
                                       'greater than the one provided. '
949
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
950
        self.parser.add_argument('-tl', '--timestamp-lt', type=str, dest='timestamp_lt',
951
                                 default=None,
952
                                 help=('Only return executions with timestamp '
953
                                       'lower than the one provided. '
954
                                       'Use time in the format "2000-01-01T12:00:00.000Z".'))
955
        self.parser.add_argument('-l', '--showall', action='store_true',
956
                                 help='')
957
958
        # Display options
959
        self.parser.add_argument('-a', '--attr', nargs='+',
960
                                 default=self.display_attributes,
961
                                 help=('List of attributes to include in the '
962
                                       'output. "all" will return all '
963
                                       'attributes.'))
964
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
965
                                 default=None,
966
                                 help=('Set the width of columns in output.'))
967
968
    @add_auth_token_to_kwargs_from_cli
969
    def run(self, args, **kwargs):
970
        # Filtering options
971
        if args.action:
972
            kwargs['action'] = args.action
973
        if args.status:
974
            kwargs['status'] = args.status
975
        if args.trigger_instance:
976
            kwargs['trigger_instance'] = args.trigger_instance
977
        if not args.showall:
978
            # null is the magic string that translates to does not exist.
979
            kwargs['parent'] = 'null'
980
        if args.timestamp_gt:
981
            kwargs['timestamp_gt'] = args.timestamp_gt
982
        if args.timestamp_lt:
983
            kwargs['timestamp_lt'] = args.timestamp_lt
984
        if args.sort_order:
985
            if args.sort_order in ['asc', 'ascending']:
986
                kwargs['sort_asc'] = True
987
            elif args.sort_order in ['desc', 'descending']:
988
                kwargs['sort_desc'] = True
989
990
        # We exclude "result" and "trigger_instance" attributes which can contain a lot of data
991
        # since they are not displayed nor used which speeds the common operation substantially.
992
        exclude_attributes = self._get_exclude_attributes(args=args)
993
        exclude_attributes = ','.join(exclude_attributes)
994
        kwargs['exclude_attributes'] = exclude_attributes
995
996
        return self.manager.query(limit=args.last, **kwargs)
997
998
    def run_and_print(self, args, **kwargs):
999
        instances = format_wf_instances(self.run(args, **kwargs))
1000
1001
        if not args.json and not args.yaml:
1002
            # Include elapsed time for running executions
1003
            instances = format_execution_statuses(instances)
1004
1005
        self.print_output(reversed(instances), table.MultiColumnTable,
1006
                          attributes=args.attr, widths=args.width,
1007
                          json=args.json,
1008
                          yaml=args.yaml,
1009
                          attribute_transform_functions=self.attribute_transform_functions)
1010
1011
1012
class ActionExecutionGetCommand(ActionRunCommandMixin, ActionExecutionReadCommand):
1013
    display_attributes = ['id', 'action.ref', 'context.user', 'parameters', 'status',
1014
                          'start_timestamp', 'end_timestamp', 'result', 'liveaction']
1015
1016
    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...
1017
        super(ActionExecutionGetCommand, self).__init__(
1018
            resource, 'get',
1019
            'Get individual %s.' % resource.get_display_name().lower(),
1020
            *args, **kwargs)
1021
1022
        self.parser.add_argument('id',
1023
                                 help=('ID of the %s.' %
1024
                                       resource.get_display_name().lower()))
1025
1026
        self._add_common_options()
1027
1028
    @add_auth_token_to_kwargs_from_cli
1029
    def run(self, args, **kwargs):
1030
        # We exclude "result" and / or "trigger_instance" attribute if it's not explicitly
1031
        # requested by user either via "--attr" flag or by default.
1032
        exclude_attributes = self._get_exclude_attributes(args=args)
1033
        exclude_attributes = ','.join(exclude_attributes)
1034
1035
        kwargs['params'] = {'exclude_attributes': exclude_attributes}
1036
1037
        execution = self.get_resource_by_id(id=args.id, **kwargs)
1038
        return execution
1039
1040
    @add_auth_token_to_kwargs_from_cli
1041
    def run_and_print(self, args, **kwargs):
1042
        try:
1043
            execution = self.run(args, **kwargs)
1044
1045
            if not args.json and not args.yaml:
1046
                # Include elapsed time for running executions
1047
                execution = format_execution_status(execution)
1048
        except resource.ResourceNotFoundError:
1049
            self.print_not_found(args.id)
1050
            raise OperationFailureException('Execution %s not found.' % (args.id))
1051
        return self._print_execution_details(execution=execution, args=args, **kwargs)
1052
1053
1054
class ActionExecutionCancelCommand(resource.ResourceCommand):
1055
1056
    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...
1057
        super(ActionExecutionCancelCommand, self).__init__(
1058
            resource, 'cancel', 'Cancel an %s.' %
1059
            resource.get_plural_display_name().lower(),
1060
            *args, **kwargs)
1061
1062
        self.parser.add_argument('id',
1063
                                 help=('ID of the %s.' %
1064
                                       resource.get_display_name().lower()))
1065
1066
    def run(self, args, **kwargs):
1067
        return self.manager.delete_by_id(args.id)
1068
1069
    @add_auth_token_to_kwargs_from_cli
1070
    def run_and_print(self, args, **kwargs):
1071
        response = self.run(args, **kwargs)
1072
        if response and 'faultstring' in response:
1073
            message = response.get('faultstring', 'Cancellation requested for %s with id %s.' %
1074
                                   (self.resource.get_display_name().lower(), args.id))
1075
1076
        elif response:
1077
            message = '%s with id %s canceled.' % (self.resource.get_display_name().lower(),
1078
                                                   args.id)
1079
        else:
1080
            message = 'Cannot cancel %s with id %s.' % (self.resource.get_display_name().lower(),
1081
                                                        args.id)
1082
        print(message)
1083
1084
1085
class ActionExecutionReRunCommand(ActionRunCommandMixin, resource.ResourceCommand):
1086
    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...
1087
1088
        super(ActionExecutionReRunCommand, self).__init__(
1089
            resource, kwargs.pop('name', 're-run'),
1090
            'A command to re-run a particular action.',
1091
            *args, **kwargs)
1092
1093
        self.parser.add_argument('id', nargs='?',
1094
                                 metavar='id',
1095
                                 help='ID of action execution to re-run ')
1096
        self.parser.add_argument('parameters', nargs='*',
1097
                                 help='List of keyword args, positional args, '
1098
                                      'and optional args for the action.')
1099
        self.parser.add_argument('--tasks', nargs='*',
1100
                                 help='Name of the workflow tasks to re-run.')
1101
        self.parser.add_argument('--no-reset', dest='no_reset', nargs='*',
1102
                                 help='Name of the with-items tasks to not reset. This only '
1103
                                      'applies to Mistral workflows. By default, all iterations '
1104
                                      'for with-items tasks is rerun. If no reset, only failed '
1105
                                      ' iterations are rerun.')
1106
        self.parser.add_argument('-a', '--async',
1107
                                 action='store_true', dest='async',
1108
                                 help='Do not wait for action to finish.')
1109
        self.parser.add_argument('-e', '--inherit-env',
1110
                                 action='store_true', dest='inherit_env',
1111
                                 help='Pass all the environment variables '
1112
                                      'which are accessible to the CLI as "env" '
1113
                                      'parameter to the action. Note: Only works '
1114
                                      'with python, local and remote runners.')
1115
        self.parser.add_argument('-h', '--help',
1116
                                 action='store_true', dest='help',
1117
                                 help='Print usage for the given action.')
1118
1119
        self._add_common_options()
1120
1121
    @add_auth_token_to_kwargs_from_cli
1122
    def run(self, args, **kwargs):
1123
        existing_execution = self.manager.get_by_id(args.id, **kwargs)
1124
1125
        if not existing_execution:
1126
            raise resource.ResourceNotFoundError('Action execution with id "%s" cannot be found.' %
1127
                                                 (args.id))
1128
1129
        action_mgr = self.app.client.managers['Action']
1130
        runner_mgr = self.app.client.managers['RunnerType']
1131
        action_exec_mgr = self.app.client.managers['LiveAction']
1132
1133
        action_ref = existing_execution.action['ref']
1134
        action = action_mgr.get_by_ref_or_id(action_ref)
1135
        runner = runner_mgr.get_by_name(action.runner_type)
1136
1137
        action_parameters = self._get_action_parameters_from_args(action=action, runner=runner,
1138
                                                                  args=args)
1139
1140
        execution = action_exec_mgr.re_run(execution_id=args.id,
1141
                                           parameters=action_parameters,
1142
                                           tasks=args.tasks,
1143
                                           no_reset=args.no_reset,
1144
                                           **kwargs)
1145
1146
        execution = self._get_execution_result(execution=execution,
1147
                                               action_exec_mgr=action_exec_mgr,
1148
                                               args=args, **kwargs)
1149
1150
        return execution
1151