Completed
Pull Request — master (#2895)
by Anthony
06:43 queued 56s
created

ActionRunCommandMixin   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 531
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 531
rs 1.5789
wmc 112

20 Methods

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