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 — develop-v1.3.1 ( 8fa207...a1cf9b )
by
unknown
06:11
created

ActionRunCommandMixin   F

Complexity

Total Complexity 117

Size/Duplication

Total Lines 551
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 551
rs 1.5789
c 0
b 0
f 0
wmc 117

21 Methods

Rating   Name   Duplication   Size   Complexity  
A transform_object() 0 18 4
F _print_execution_details() 0 39 10
A get_resource() 0 2 1
B _format_for_common_representation() 0 19 5
B _get_execution_result() 0 22 5
A read_file() 0 11 4
B _add_common_options() 0 33 1
A _get_inherited_env_vars() 0 10 4
B normalize() 0 11 7
A run_and_print() 0 15 4
F _format_child_instances() 0 33 9
B _get_task_error() 0 22 4
A _sort_parameters() 0 13 2
C _run_and_print_child_task_list() 0 59 8
A _get_parameter_sort_value() 0 15 2
A _get_top_level_error() 0 14 2
C _get_params_types() 0 25 7
C _print_param() 0 20 10
F _get_action_parameters_from_args() 0 126 31
F _print_help() 0 50 12
A is_immutable() 0 6 2

How to fix   Complexity   

Complex Class

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