Passed
Push — develop ( df8237...b0ff90 )
by Plexxi
06:18 queued 03:16
created

TraceListCommand.run()   C

Complexity

Conditions 8

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 0
loc 19
rs 6.6666
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
from st2client.models import Resource, Trace, TriggerInstance, Rule, LiveAction
17
from st2client.exceptions.operations import OperationFailureException
18
from st2client.formatters import table
19
from st2client.formatters import execution as execution_formatter
20
from st2client.commands import resource
21
from st2client.utils.date import format_isodate_for_user_timezone
22
23
24
TRACE_ATTRIBUTE_DISPLAY_ORDER = ['id', 'trace_tag', 'action_executions', 'rules',
25
                                 'trigger_instances', 'start_timestamp']
26
27
TRACE_HEADER_DISPLAY_ORDER = ['id', 'trace_tag', 'start_timestamp']
28
29
TRACE_COMPONENT_DISPLAY_LABELS = ['id', 'type', 'ref', 'updated_at']
30
31
TRACE_DISPLAY_ATTRIBUTES = ['all']
32
33
TRIGGER_INSTANCE_DISPLAY_OPTIONS = [
34
    'all',
35
    'trigger-instances',
36
    'trigger_instances',
37
    'triggerinstances',
38
    'triggers'
39
]
40
41
ACTION_EXECUTION_DISPLAY_OPTIONS = [
42
    'all',
43
    'executions',
44
    'action-executions',
45
    'action_executions',
46
    'actionexecutions',
47
    'actions'
48
]
49
50
51
class TraceBranch(resource.ResourceBranch):
52
    def __init__(self, description, app, subparsers, parent_parser=None):
53
        super(TraceBranch, self).__init__(
54
            Trace, description, app, subparsers,
55
            parent_parser=parent_parser,
56
            read_only=True,
57
            commands={
58
                'list': TraceListCommand,
59
                'get': TraceGetCommand
60
            })
61
62
63
class SingleTraceDisplayMixin(object):
64
65
    def print_trace_details(self, trace, args, **kwargs):
66
        options = {'attributes': TRACE_ATTRIBUTE_DISPLAY_ORDER if args.json else
67
                   TRACE_HEADER_DISPLAY_ORDER}
68
        options['json'] = args.json
69
        options['yaml'] = args.yaml
70
        options['attribute_transform_functions'] = self.attribute_transform_functions
71
72
        formatter = execution_formatter.ExecutionResult
73
74
        self.print_output(trace, formatter, **options)
75
76
        # Everything should be printed if we are printing json.
77
        if args.json or args.yaml:
78
            return
79
80
        components = []
81
        if any(attr in args.attr for attr in TRIGGER_INSTANCE_DISPLAY_OPTIONS):
82
            components.extend([Resource(**{'id': trigger_instance['object_id'],
83
                                           'type': TriggerInstance._alias.lower(),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _alias was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
84
                                           'ref': trigger_instance['ref'],
85
                                           'updated_at': trigger_instance['updated_at']})
86
                               for trigger_instance in trace.trigger_instances])
87
        if any(attr in args.attr for attr in ['all', 'rules']):
88
            components.extend([Resource(**{'id': rule['object_id'],
89
                                           'type': Rule._alias.lower(),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _alias was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
90
                                           'ref': rule['ref'],
91
                                           'updated_at': rule['updated_at']})
92
                               for rule in trace.rules])
93
        if any(attr in args.attr for attr in ACTION_EXECUTION_DISPLAY_OPTIONS):
94
            components.extend([Resource(**{'id': execution['object_id'],
95
                                           'type': LiveAction._alias.lower(),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _alias was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
96
                                           'ref': execution['ref'],
97
                                           'updated_at': execution['updated_at']})
98
                               for execution in trace.action_executions])
99
        if components:
100
            components.sort(key=lambda resource: resource.updated_at)
101
            self.print_output(components, table.MultiColumnTable,
102
                              attributes=TRACE_COMPONENT_DISPLAY_LABELS,
103
                              json=args.json, yaml=args.yaml)
104
105
106
class TraceListCommand(resource.ResourceCommand, SingleTraceDisplayMixin):
107
    display_attributes = ['id', 'trace_tag', 'start_timestamp']
108
109
    attribute_transform_functions = {
110
        'start_timestamp': format_isodate_for_user_timezone
111
    }
112
113
    attribute_display_order = TRACE_ATTRIBUTE_DISPLAY_ORDER
114
115
    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 20).

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...
116
        super(TraceListCommand, self).__init__(
117
            resource, 'list', 'Get the list of the 50 most recent %s.' %
118
            resource.get_plural_display_name().lower(),
119
            *args, **kwargs)
120
121
        self.group = self.parser.add_mutually_exclusive_group()
122
        self.parser.add_argument('-n', '--last', type=int, dest='last',
123
                                 default=50,
124
                                 help=('List N most recent %s.' %
125
                                       resource.get_plural_display_name().lower()))
126
        self.parser.add_argument('-s', '--sort', type=str, dest='sort_order',
127
                                 default='descending',
128
                                 help=('Sort %s by start timestamp, '
129
                                       'asc|ascending (earliest first) '
130
                                       'or desc|descending (latest first)' %
131
                                       resource.get_plural_display_name().lower()))
132
133
        # Filter options
134
        self.group.add_argument('-c', '--trace-tag', help='Trace-tag to filter the list.')
135
        self.group.add_argument('-e', '--execution', help='Execution to filter the list.')
136
        self.group.add_argument('-r', '--rule', help='Rule to filter the list.')
137
        self.group.add_argument('-g', '--trigger-instance',
138
                                help='TriggerInstance to filter the list.')
139
        # Display options
140
        self.parser.add_argument('-a', '--attr', nargs='+',
141
                                 default=self.display_attributes,
142
                                 help=('List of attributes to include in the '
143
                                       'output. "all" will return all '
144
                                       'attributes.'))
145
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
146
                                 default=None,
147
                                 help=('Set the width of columns in output.'))
148
149
    @resource.add_auth_token_to_kwargs_from_cli
150
    def run(self, args, **kwargs):
151
        # Filtering options
152
        if args.trace_tag:
153
            kwargs['trace_tag'] = args.trace_tag
154
        if args.trigger_instance:
155
            kwargs['trigger_instance'] = args.trigger_instance
156
        if args.execution:
157
            kwargs['execution'] = args.execution
158
        if args.rule:
159
            kwargs['rule'] = args.rule
160
161
        if args.sort_order:
162
            if args.sort_order in ['asc', 'ascending']:
163
                kwargs['sort_asc'] = True
164
            elif args.sort_order in ['desc', 'descending']:
165
                kwargs['sort_desc'] = True
166
167
        return self.manager.query(limit=args.last, **kwargs)
168
169
    def run_and_print(self, args, **kwargs):
170
        instances = self.run(args, **kwargs)
171
        if instances and len(instances) == 1:
172
            # For a single Trace we must include the components unless
173
            # user has overriden the attributes to display
174
            if args.attr == self.display_attributes:
175
                args.attr = ['all']
176
            self.print_trace_details(trace=instances[0], args=args)
177
        else:
178
            self.print_output(reversed(instances), table.MultiColumnTable,
179
                              attributes=args.attr, widths=args.width,
180
                              json=args.json, yaml=args.yaml,
181
                              attribute_transform_functions=self.attribute_transform_functions)
182
183
184
class TraceGetCommand(resource.ResourceGetCommand, SingleTraceDisplayMixin):
185
    display_attributes = ['all']
186
    attribute_display_order = TRACE_ATTRIBUTE_DISPLAY_ORDER
187
    attribute_transform_functions = {
188
        'start_timestamp': format_isodate_for_user_timezone
189
    }
190
191
    pk_argument_name = 'id'
192
193
    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 20).

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...
194
        super(TraceGetCommand, self).__init__(resource, *args, **kwargs)
195
196
        # Causation chains
197
        self.causation_group = self.parser.add_mutually_exclusive_group()
198
199
        self.causation_group.add_argument('-e', '--execution',
200
                                          help='Execution to show causation chain.')
201
        self.causation_group.add_argument('-r', '--rule', help='Rule to show causation chain.')
202
        self.causation_group.add_argument('-g', '--trigger-instance',
203
                                          help='TriggerInstance to show causation chain.')
204
205
        # display filter group
206
        self.display_filter_group = self.parser.add_argument_group()
207
208
        self.display_filter_group.add_argument('--show-executions', action='store_true',
209
                                              help='Only show executions.')
210
        self.display_filter_group.add_argument('--show-rules', action='store_true',
211
                                              help='Only show rules.')
212
        self.display_filter_group.add_argument('--show-trigger-instances', action='store_true',
213
                                              help='Only show trigger instances.')
214
        self.display_filter_group.add_argument('-n', '--hide-noop-triggers', action='store_true',
215
                                              help='Hide noop trigger instances.')
216
217
    @resource.add_auth_token_to_kwargs_from_cli
218
    def run(self, args, **kwargs):
219
        resource_id = getattr(args, self.pk_argument_name, None)
220
        return self.get_resource_by_id(resource_id, **kwargs)
221
222
    @resource.add_auth_token_to_kwargs_from_cli
223
    def run_and_print(self, args, **kwargs):
224
        trace = None
225
        try:
226
            trace = self.run(args, **kwargs)
227
        except resource.ResourceNotFoundError:
228
            self.print_not_found(args.id)
229
            raise OperationFailureException('Trace %s not found.' % (args.id))
230
        # First filter for causation chains
231
        trace = self._filter_trace_components(trace=trace, args=args)
232
        # next filter for display purposes
233
        trace = self._apply_display_filters(trace=trace, args=args)
234
        return self.print_trace_details(trace=trace, args=args)
235
236
    @staticmethod
237
    def _filter_trace_components(trace, args):
238
        """
239
        This function walks up the component causal chain. It only returns
240
        properties in the causal chain and nothing else.
241
        """
242
        # check if any filtering is desired
243
        if not (args.execution or args.rule or args.trigger_instance):
244
            return trace
245
246
        component_id = None
247
        component_type = None
248
249
        # pick the right component type
250
        if args.execution:
251
            component_id = args.execution
252
            component_type = 'action_execution'
253
        elif args.rule:
254
            component_id = args.rule
255
            component_type = 'rule'
256
        elif args.trigger_instance:
257
            component_id = args.trigger_instance
258
            component_type = 'trigger_instance'
259
260
        # Initialize collection to use
261
        action_executions = []
262
        rules = []
263
        trigger_instances = []
264
265
        # setup flag to properly manage termination conditions
266
        search_target_found = component_id and component_type
267
268
        while search_target_found:
269
            components_list = []
270
            if component_type == 'action_execution':
271
                components_list = trace.action_executions
272
                to_update_list = action_executions
273
            elif component_type == 'rule':
274
                components_list = trace.rules
275
                to_update_list = rules
276
            elif component_type == 'trigger_instance':
277
                components_list = trace.trigger_instances
278
                to_update_list = trigger_instances
279
            # Look for search_target in the right collection and
280
            # once found look up the caused_by to keep movig up
281
            # the chain.
282
            search_target_found = False
283
            # init to default value
284
            component_caused_by_id = None
285
            for component in components_list:
286
                test_id = component['object_id']
287
                if test_id == component_id:
288
                    caused_by = component.get('caused_by', {})
289
                    component_id = caused_by.get('id', None)
290
                    component_type = caused_by.get('type', None)
291
                    # If provided the component_caused_by_id must match as well. This is mostly
292
                    # applicable for rules since the same rule may appear multiple times and can
293
                    # only be distinguished by causing TriggerInstance.
294
                    if component_caused_by_id and component_caused_by_id != component_id:
295
                        continue
296
                    component_caused_by_id = None
297
                    to_update_list.append(component)
298
                    # In some cases the component_id and the causing component are combined to
299
                    # provide the complete causation chain. Think rule + triggerinstance
300
                    if component_id and ':' in component_id:
301
                        component_id_split = component_id.split(':')
302
                        component_id = component_id_split[0]
303
                        component_caused_by_id = component_id_split[1]
304
                    search_target_found = True
305
                    break
306
307
        trace.action_executions = action_executions
308
        trace.rules = rules
309
        trace.trigger_instances = trigger_instances
310
        return trace
311
312
    @staticmethod
313
    def _apply_display_filters(trace, args):
314
        """
315
        This function looks at the disaply filters to determine which components
316
        should be displayed.
317
        """
318
        # If all the filters are false nothing is to be filtered.
319
        all_component_types = not(args.show_executions or
320
                                  args.show_rules or
321
                                  args.show_trigger_instances)
322
323
        # check if noop_triggers are to be hidden. This check applies whenever TriggerInstances
324
        # are to be shown.
325
        if (all_component_types or args.show_trigger_instances) and args.hide_noop_triggers:
326
            filtered_trigger_instances = []
327
            for trigger_instance in trace.trigger_instances:
328
                is_noop_trigger_instance = True
329
                for rule in trace.rules:
330
                    caused_by_id = rule.get('caused_by', {}).get('id', None)
331
                    if caused_by_id == trigger_instance['object_id']:
332
                        is_noop_trigger_instance = False
333
                if not is_noop_trigger_instance:
334
                    filtered_trigger_instances.append(trigger_instance)
335
            trace.trigger_instances = filtered_trigger_instances
336
337
        if all_component_types:
338
            return trace
339
340
        if not args.show_executions:
341
            trace.action_executions = []
342
343
        if not args.show_rules:
344
            trace.rules = []
345
346
        if not args.show_trigger_instances:
347
            trace.trigger_instances = []
348
349
        return trace
350