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 ( 107d51...1485cb )
by Plexxi
12:34 queued 05:57
created

ActionRunCommandMixin.transform_array()   A

Complexity

Conditions 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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