Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/history/show.py
1567 views
1
# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
import datetime
14
import json
15
import sys
16
import xml.parsers.expat
17
import xml.dom.minidom
18
19
import colorama
20
21
from awscli.table import COLORAMA_KWARGS
22
from awscli.customizations.history.commands import HistorySubcommand
23
from awscli.customizations.history.filters import RegexFilter
24
25
26
class Formatter(object):
27
def __init__(self, output=None, include=None, exclude=None):
28
"""Formats and outputs CLI history events
29
30
:type output: File-like obj
31
:param output: The stream to write the formatted event to. By default
32
sys.stdout is used.
33
34
:type include: list
35
:param include: A filter specifying which event to only be displayed.
36
This parameter is mutually exclusive with exclude.
37
38
:type exclude: list
39
:param exclude: A filter specifying which events to exclude from being
40
displayed. This parameter is mutually exclusive with include.
41
42
"""
43
self._output = output
44
if self._output is None:
45
self._output = sys.stdout
46
if include and exclude:
47
raise ValueError(
48
'Either input or exclude can be provided but not both')
49
self._include = include
50
self._exclude = exclude
51
52
def display(self, event_record):
53
"""Displays a formatted version of the event record
54
55
:type event_record: dict
56
:param event_record: The event record to format and display.
57
"""
58
if self._should_display(event_record):
59
self._display(event_record)
60
61
def _display(self, event_record):
62
raise NotImplementedError('_display()')
63
64
def _should_display(self, event_record):
65
if self._include:
66
return event_record['event_type'] in self._include
67
elif self._exclude:
68
return event_record['event_type'] not in self._exclude
69
else:
70
return True
71
72
73
class DetailedFormatter(Formatter):
74
_SIG_FILTER = RegexFilter(
75
'Signature=([a-z0-9]{4})[a-z0-9]{60}',
76
r'Signature=\1...',
77
)
78
79
_SECTIONS = {
80
'CLI_VERSION': {
81
'title': 'AWS CLI command entered',
82
'values': [
83
{'description': 'with AWS CLI version'}
84
]
85
},
86
'CLI_ARGUMENTS': {
87
'values': [
88
{'description': 'with arguments'}
89
]
90
},
91
'API_CALL': {
92
'title': 'API call made',
93
'values': [
94
{
95
'description': 'to service',
96
'payload_key': 'service'
97
},
98
{
99
'description': 'using operation',
100
'payload_key': 'operation'
101
},
102
{
103
'description': 'with parameters',
104
'payload_key': 'params',
105
'value_format': 'dictionary'
106
}
107
]
108
},
109
'HTTP_REQUEST': {
110
'title': 'HTTP request sent',
111
'values': [
112
{
113
'description': 'to URL',
114
'payload_key': 'url'
115
},
116
{
117
'description': 'with method',
118
'payload_key': 'method'
119
},
120
{
121
'description': 'with headers',
122
'payload_key': 'headers',
123
'value_format': 'dictionary',
124
'filters': [_SIG_FILTER]
125
},
126
{
127
'description': 'with body',
128
'payload_key': 'body',
129
'value_format': 'http_body'
130
}
131
132
]
133
},
134
'HTTP_RESPONSE': {
135
'title': 'HTTP response received',
136
'values': [
137
{
138
'description': 'with status code',
139
'payload_key': 'status_code'
140
},
141
{
142
'description': 'with headers',
143
'payload_key': 'headers',
144
'value_format': 'dictionary'
145
},
146
{
147
'description': 'with body',
148
'payload_key': 'body',
149
'value_format': 'http_body'
150
}
151
]
152
},
153
'PARSED_RESPONSE': {
154
'title': 'HTTP response parsed',
155
'values': [
156
{
157
'description': 'parsed to',
158
'value_format': 'dictionary'
159
}
160
]
161
},
162
'CLI_RC': {
163
'title': 'AWS CLI command exited',
164
'values': [
165
{'description': 'with return code'}
166
]
167
},
168
}
169
170
_COMPONENT_COLORS = {
171
'title': colorama.Style.BRIGHT,
172
'description': colorama.Fore.CYAN
173
}
174
175
def __init__(self, output=None, include=None, exclude=None, colorize=True):
176
super(DetailedFormatter, self).__init__(output, include, exclude)
177
self._request_id_to_api_num = {}
178
self._num_api_calls = 0
179
self._colorize = colorize
180
self._value_pformatter = SectionValuePrettyFormatter()
181
if self._colorize:
182
colorama.init(**COLORAMA_KWARGS)
183
184
def _display(self, event_record):
185
section_definition = self._SECTIONS.get(event_record['event_type'])
186
if section_definition is not None:
187
self._display_section(event_record, section_definition)
188
189
def _display_section(self, event_record, section_definition):
190
if 'title' in section_definition:
191
self._display_title(section_definition['title'], event_record)
192
for value_definition in section_definition['values']:
193
self._display_value(value_definition, event_record)
194
195
def _display_title(self, title, event_record):
196
formatted_title = self._format_section_title(title, event_record)
197
self._write_output(formatted_title)
198
199
def _display_value(self, value_definition, event_record):
200
value_description = value_definition['description']
201
event_record_payload = event_record['payload']
202
value = event_record_payload
203
if 'payload_key' in value_definition:
204
value = event_record_payload[value_definition['payload_key']]
205
formatted_value = self._format_description(value_description)
206
formatted_value += self._format_value(
207
value, event_record, value_definition.get('value_format')
208
)
209
if 'filters' in value_definition:
210
for text_filter in value_definition['filters']:
211
formatted_value = text_filter.filter_text(formatted_value)
212
self._write_output(formatted_value)
213
214
def _write_output(self, content):
215
if isinstance(content, str):
216
content = content.encode('utf-8')
217
self._output.write(content)
218
219
def _format_section_title(self, title, event_record):
220
formatted_title = title
221
api_num = self._get_api_num(event_record)
222
if api_num is not None:
223
formatted_title = ('[%s] ' % api_num) + formatted_title
224
formatted_title = self._color_if_configured(formatted_title, 'title')
225
formatted_title += '\n'
226
227
formatted_timestamp = self._format_description('at time')
228
formatted_timestamp += self._format_value(
229
event_record['timestamp'], event_record, value_format='timestamp')
230
231
return '\n' + formatted_title + formatted_timestamp
232
233
def _get_api_num(self, event_record):
234
request_id = event_record['request_id']
235
if request_id:
236
if request_id not in self._request_id_to_api_num:
237
self._request_id_to_api_num[
238
request_id] = self._num_api_calls
239
self._num_api_calls += 1
240
return self._request_id_to_api_num[request_id]
241
242
def _format_description(self, value_description):
243
return self._color_if_configured(
244
value_description + ': ', 'description')
245
246
def _format_value(self, value, event_record, value_format=None):
247
if value_format:
248
formatted_value = self._value_pformatter.pformat(
249
value, value_format, event_record)
250
else:
251
formatted_value = str(value)
252
return formatted_value + '\n'
253
254
def _color_if_configured(self, text, component):
255
if self._colorize:
256
color = self._COMPONENT_COLORS[component]
257
return color + text + colorama.Style.RESET_ALL
258
return text
259
260
261
class SectionValuePrettyFormatter(object):
262
def pformat(self, value, value_format, event_record):
263
return getattr(self, '_pformat_' + value_format)(value, event_record)
264
265
def _pformat_timestamp(self, event_timestamp, event_record=None):
266
return datetime.datetime.fromtimestamp(
267
event_timestamp/1000.0).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
268
269
def _pformat_dictionary(self, obj, event_record=None):
270
return json.dumps(obj=obj, sort_keys=True, indent=4)
271
272
def _pformat_http_body(self, body, event_record):
273
if not body:
274
return 'There is no associated body'
275
elif event_record['payload'].get('streaming', False):
276
return 'The body is a stream and will not be displayed'
277
elif self._is_xml(body):
278
# TODO: Figure out a way to minimize the number of times we have
279
# to parse the XML. Currently at worst, it will take three times.
280
# One to determine if it is XML, another to strip whitespace, and
281
# a third to convert to make it pretty. This is an issue as it
282
# can cause issues when there are large XML payloads such as
283
# an s3 ListObjects call.
284
return self._get_pretty_xml(body)
285
elif self._is_json_structure(body):
286
return self._get_pretty_json(body)
287
else:
288
return body
289
290
def _get_pretty_xml(self, body):
291
# The body is parsed and whitespace is stripped because some services
292
# like ec2 already return pretty XML and if toprettyxml() was applied
293
# to it, it will add even more newlines and spaces on top of it.
294
# So this just removes all whitespace from the start to prevent the
295
# chance of adding to much newlines and spaces when toprettyxml()
296
# is called.
297
stripped_body = self._strip_whitespace(body)
298
xml_dom = xml.dom.minidom.parseString(stripped_body)
299
return xml_dom.toprettyxml(indent=' '*4, newl='\n')
300
301
def _get_pretty_json(self, body):
302
# The json body is loaded so it can be dumped in a format that
303
# is desired.
304
obj = json.loads(body)
305
return self._pformat_dictionary(obj)
306
307
def _is_xml(self, body):
308
try:
309
xml.dom.minidom.parseString(body)
310
except xml.parsers.expat.ExpatError:
311
return False
312
return True
313
314
def _strip_whitespace(self, xml_string):
315
xml_dom = xml.dom.minidom.parseString(xml_string)
316
return ''.join(
317
[line.strip() for line in xml_dom.toxml().splitlines()]
318
)
319
320
def _is_json_structure(self, body):
321
if body.startswith('{'):
322
try:
323
json.loads(body)
324
return True
325
except json.decoder.JSONDecodeError:
326
return False
327
return False
328
329
330
class ShowCommand(HistorySubcommand):
331
NAME = 'show'
332
DESCRIPTION = (
333
'Shows the various events related to running a specific CLI command. '
334
'If this command is ran without any positional arguments, it will '
335
'display the events for the last CLI command ran.'
336
)
337
FORMATTERS = {
338
'detailed': DetailedFormatter
339
}
340
ARG_TABLE = [
341
{'name': 'command_id', 'nargs': '?', 'default': 'latest',
342
'positional_arg': True,
343
'help_text': (
344
'The ID of the CLI command to show. If this positional argument '
345
'is omitted, it will show the last the CLI command ran.')},
346
{'name': 'include', 'nargs': '+',
347
'help_text': (
348
'Specifies which events to **only** include when showing the '
349
'CLI command. This argument is mutually exclusive with '
350
'``--exclude``.')},
351
{'name': 'exclude', 'nargs': '+',
352
'help_text': (
353
'Specifies which events to exclude when showing the '
354
'CLI command. This argument is mutually exclusive with '
355
'``--include``.')},
356
{'name': 'format', 'choices': FORMATTERS.keys(),
357
'default': 'detailed', 'help_text': (
358
'Specifies which format to use in showing the events for '
359
'the specified CLI command. The following formats are '
360
'supported:\n\n'
361
'<ul>'
362
'<li> detailed - This the default format. It prints out a '
363
'detailed overview of the CLI command ran. It displays all '
364
'of the key events in the command lifecycle where each '
365
'important event has a title and its important values '
366
'underneath. The events are ordered by timestamp and events of '
367
'the same API call are associated together with the '
368
'[``api_id``] notation where events that share the same '
369
'``api_id`` belong to the lifecycle of the same API call.'
370
'</li>'
371
'</ul>'
372
)
373
}
374
]
375
376
def _run_main(self, parsed_args, parsed_globals):
377
self._connect_to_history_db()
378
try:
379
self._validate_args(parsed_args)
380
with self._get_output_stream() as output_stream:
381
formatter = self._get_formatter(
382
parsed_args, parsed_globals, output_stream)
383
for record in self._get_record_iterator(parsed_args):
384
formatter.display(record)
385
finally:
386
self._close_history_db()
387
return 0
388
389
def _validate_args(self, parsed_args):
390
if parsed_args.exclude and parsed_args.include:
391
raise ValueError(
392
'Either --exclude or --include can be provided but not both')
393
394
def _get_formatter(self, parsed_args, parsed_globals, output_stream):
395
format_type = parsed_args.format
396
formatter_kwargs = {
397
'include': parsed_args.include,
398
'exclude': parsed_args.exclude,
399
'output': output_stream
400
}
401
if format_type == 'detailed':
402
formatter_kwargs['colorize'] = self._should_use_color(
403
parsed_globals)
404
return self.FORMATTERS[format_type](**formatter_kwargs)
405
406
def _get_record_iterator(self, parsed_args):
407
if parsed_args.command_id == 'latest':
408
return self._db_reader.iter_latest_records()
409
else:
410
return self._db_reader.iter_records(parsed_args.command_id)
411
412