Passed
Push — develop ( 481794...37081d )
by Plexxi
12:07 queued 09:42
created

ActionRunCommandMixin.read_file()   A

Complexity

Conditions 4

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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