Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/clidriver.py
1566 views
1
# Copyright 2012-2013 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 logging
14
import signal
15
import sys
16
17
import botocore.session
18
from botocore.compat import OrderedDict, copy_kwargs
19
from botocore.exceptions import (
20
NoCredentialsError,
21
NoRegionError,
22
ProfileNotFound,
23
)
24
from botocore.history import get_global_history_recorder
25
26
from awscli import EnvironmentVariables, __version__
27
from awscli.alias import AliasCommandInjector, AliasLoader
28
from awscli.argparser import (
29
USAGE,
30
ArgTableArgParser,
31
MainArgParser,
32
ServiceArgParser,
33
)
34
from awscli.argprocess import unpack_argument
35
from awscli.arguments import (
36
BooleanArgument,
37
CLIArgument,
38
CustomArgument,
39
ListArgument,
40
UnknownArgumentError,
41
)
42
from awscli.commands import CLICommand
43
from awscli.compat import get_stderr_text_writer
44
from awscli.formatter import get_formatter
45
from awscli.help import (
46
OperationHelpCommand,
47
ProviderHelpCommand,
48
ServiceHelpCommand,
49
)
50
from awscli.plugin import load_plugins
51
from awscli.utils import emit_top_level_args_parsed_event, write_exception, create_nested_client
52
from botocore import __version__ as botocore_version
53
from botocore import xform_name
54
55
LOG = logging.getLogger('awscli.clidriver')
56
LOG_FORMAT = (
57
'%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s'
58
)
59
HISTORY_RECORDER = get_global_history_recorder()
60
# Don't remove this line. The idna encoding
61
# is used by getaddrinfo when dealing with unicode hostnames,
62
# and in some cases, there appears to be a race condition
63
# where threads will get a LookupError on getaddrinfo() saying
64
# that the encoding doesn't exist. Using the idna encoding before
65
# running any CLI code (and any threads it may create) ensures that
66
# the encodings.idna is imported and registered in the codecs registry,
67
# which will stop the LookupErrors from happening.
68
# See: https://bugs.python.org/issue29288
69
''.encode('idna')
70
71
72
def main():
73
driver = create_clidriver()
74
rc = driver.main()
75
HISTORY_RECORDER.record('CLI_RC', rc, 'CLI')
76
return rc
77
78
79
def create_clidriver():
80
session = botocore.session.Session(EnvironmentVariables)
81
_set_user_agent_for_session(session)
82
load_plugins(
83
session.full_config.get('plugins', {}),
84
event_hooks=session.get_component('event_emitter'),
85
)
86
driver = CLIDriver(session=session)
87
return driver
88
89
90
def _set_user_agent_for_session(session):
91
session.user_agent_name = 'aws-cli'
92
session.user_agent_version = __version__
93
session.user_agent_extra = 'botocore/%s' % botocore_version
94
95
96
class CLIDriver:
97
def __init__(self, session=None):
98
if session is None:
99
self.session = botocore.session.get_session(EnvironmentVariables)
100
_set_user_agent_for_session(self.session)
101
else:
102
self.session = session
103
self._cli_data = None
104
self._command_table = None
105
self._argument_table = None
106
self.alias_loader = AliasLoader()
107
108
def _get_cli_data(self):
109
# Not crazy about this but the data in here is needed in
110
# several places (e.g. MainArgParser, ProviderHelp) so
111
# we load it here once.
112
if self._cli_data is None:
113
self._cli_data = self.session.get_data('cli')
114
return self._cli_data
115
116
def _get_command_table(self):
117
if self._command_table is None:
118
self._command_table = self._build_command_table()
119
return self._command_table
120
121
def _get_argument_table(self):
122
if self._argument_table is None:
123
self._argument_table = self._build_argument_table()
124
return self._argument_table
125
126
def _build_command_table(self):
127
"""
128
Create the main parser to handle the global arguments.
129
130
:rtype: ``argparser.ArgumentParser``
131
:return: The parser object
132
133
"""
134
command_table = self._build_builtin_commands(self.session)
135
self.session.emit(
136
'building-command-table.main',
137
command_table=command_table,
138
session=self.session,
139
command_object=self,
140
)
141
return command_table
142
143
def _build_builtin_commands(self, session):
144
commands = OrderedDict()
145
services = session.get_available_services()
146
for service_name in services:
147
commands[service_name] = ServiceCommand(
148
cli_name=service_name,
149
session=self.session,
150
service_name=service_name,
151
)
152
return commands
153
154
def _add_aliases(self, command_table, parser):
155
injector = AliasCommandInjector(self.session, self.alias_loader)
156
injector.inject_aliases(command_table, parser)
157
158
def _build_argument_table(self):
159
argument_table = OrderedDict()
160
cli_data = self._get_cli_data()
161
cli_arguments = cli_data.get('options', None)
162
for option in cli_arguments:
163
option_params = copy_kwargs(cli_arguments[option])
164
cli_argument = self._create_cli_argument(option, option_params)
165
cli_argument.add_to_arg_table(argument_table)
166
# Then the final step is to send out an event so handlers
167
# can add extra arguments or modify existing arguments.
168
self.session.emit(
169
'building-top-level-params', argument_table=argument_table
170
)
171
return argument_table
172
173
def _create_cli_argument(self, option_name, option_params):
174
return CustomArgument(
175
option_name,
176
help_text=option_params.get('help', ''),
177
dest=option_params.get('dest'),
178
default=option_params.get('default'),
179
action=option_params.get('action'),
180
required=option_params.get('required'),
181
choices=option_params.get('choices'),
182
cli_type_name=option_params.get('type'),
183
)
184
185
def create_help_command(self):
186
cli_data = self._get_cli_data()
187
return ProviderHelpCommand(
188
self.session,
189
self._get_command_table(),
190
self._get_argument_table(),
191
cli_data.get('description', None),
192
cli_data.get('synopsis', None),
193
cli_data.get('help_usage', None),
194
)
195
196
def _create_parser(self, command_table):
197
# Also add a 'help' command.
198
command_table['help'] = self.create_help_command()
199
cli_data = self._get_cli_data()
200
parser = MainArgParser(
201
command_table,
202
self.session.user_agent(),
203
cli_data.get('description', None),
204
self._get_argument_table(),
205
prog="aws",
206
)
207
return parser
208
209
def main(self, args=None):
210
"""
211
212
:param args: List of arguments, with the 'aws' removed. For example,
213
the command "aws s3 list-objects --bucket foo" will have an
214
args list of ``['s3', 'list-objects', '--bucket', 'foo']``.
215
216
"""
217
if args is None:
218
args = sys.argv[1:]
219
command_table = self._get_command_table()
220
parser = self._create_parser(command_table)
221
self._add_aliases(command_table, parser)
222
parsed_args, remaining = parser.parse_known_args(args)
223
try:
224
# Because _handle_top_level_args emits events, it's possible
225
# that exceptions can be raised, which should have the same
226
# general exception handling logic as calling into the
227
# command table. This is why it's in the try/except clause.
228
self._handle_top_level_args(parsed_args)
229
self._emit_session_event(parsed_args)
230
HISTORY_RECORDER.record(
231
'CLI_VERSION', self.session.user_agent(), 'CLI'
232
)
233
HISTORY_RECORDER.record('CLI_ARGUMENTS', args, 'CLI')
234
return command_table[parsed_args.command](remaining, parsed_args)
235
except UnknownArgumentError as e:
236
sys.stderr.write("usage: %s\n" % USAGE)
237
sys.stderr.write(str(e))
238
sys.stderr.write("\n")
239
return 255
240
except NoRegionError as e:
241
msg = (
242
'%s You can also configure your region by running '
243
'"aws configure".' % e
244
)
245
self._show_error(msg)
246
return 255
247
except NoCredentialsError as e:
248
msg = (
249
f'{e}. You can configure credentials by running "aws configure".'
250
)
251
self._show_error(msg)
252
return 255
253
except KeyboardInterrupt:
254
# Shell standard for signals that terminate
255
# the process is to return 128 + signum, in this case
256
# SIGINT=2, so we'll have an RC of 130.
257
sys.stdout.write("\n")
258
return 128 + signal.SIGINT
259
except Exception as e:
260
LOG.debug("Exception caught in main()", exc_info=True)
261
LOG.debug("Exiting with rc 255")
262
write_exception(e, outfile=get_stderr_text_writer())
263
return 255
264
265
def _emit_session_event(self, parsed_args):
266
# This event is guaranteed to run after the session has been
267
# initialized and a profile has been set. This was previously
268
# problematic because if something in CLIDriver caused the
269
# session components to be reset (such as session.profile = foo)
270
# then all the prior registered components would be removed.
271
self.session.emit(
272
'session-initialized',
273
session=self.session,
274
parsed_args=parsed_args,
275
)
276
277
def _show_error(self, msg):
278
LOG.debug(msg, exc_info=True)
279
sys.stderr.write(msg)
280
sys.stderr.write('\n')
281
282
def _handle_top_level_args(self, args):
283
emit_top_level_args_parsed_event(self.session, args)
284
if args.profile:
285
self.session.set_config_variable('profile', args.profile)
286
if args.region:
287
self.session.set_config_variable('region', args.region)
288
if args.debug:
289
# TODO:
290
# Unfortunately, by setting debug mode here, we miss out
291
# on all of the debug events prior to this such as the
292
# loading of plugins, etc.
293
self.session.set_stream_logger(
294
'botocore', logging.DEBUG, format_string=LOG_FORMAT
295
)
296
self.session.set_stream_logger(
297
'awscli', logging.DEBUG, format_string=LOG_FORMAT
298
)
299
self.session.set_stream_logger(
300
's3transfer', logging.DEBUG, format_string=LOG_FORMAT
301
)
302
self.session.set_stream_logger(
303
'urllib3', logging.DEBUG, format_string=LOG_FORMAT
304
)
305
LOG.debug("CLI version: %s", self.session.user_agent())
306
LOG.debug("Arguments entered to CLI: %s", sys.argv[1:])
307
308
else:
309
self.session.set_stream_logger(
310
logger_name='awscli', log_level=logging.ERROR
311
)
312
313
314
class ServiceCommand(CLICommand):
315
"""A service command for the CLI.
316
317
For example, ``aws ec2 ...`` we'd create a ServiceCommand
318
object that represents the ec2 service.
319
320
"""
321
322
def __init__(self, cli_name, session, service_name=None):
323
# The cli_name is the name the user types, the name we show
324
# in doc, etc.
325
# The service_name is the name we used internally with botocore.
326
# For example, we have the 's3api' as the cli_name for the service
327
# but this is actually bound to the 's3' service name in botocore,
328
# i.e. we load s3.json from the botocore data dir. Most of
329
# the time these are the same thing but in the case of renames,
330
# we want users/external things to be able to rename the cli name
331
# but *not* the service name, as this has to be exactly what
332
# botocore expects.
333
self._name = cli_name
334
self.session = session
335
self._command_table = None
336
if service_name is None:
337
# Then default to using the cli name.
338
self._service_name = cli_name
339
else:
340
self._service_name = service_name
341
self._lineage = [self]
342
self._service_model = None
343
344
@property
345
def name(self):
346
return self._name
347
348
@name.setter
349
def name(self, value):
350
self._name = value
351
352
@property
353
def service_model(self):
354
return self._get_service_model()
355
356
@property
357
def lineage(self):
358
return self._lineage
359
360
@lineage.setter
361
def lineage(self, value):
362
self._lineage = value
363
364
def _get_command_table(self):
365
if self._command_table is None:
366
self._command_table = self._create_command_table()
367
return self._command_table
368
369
def _get_service_model(self):
370
if self._service_model is None:
371
try:
372
api_version = self.session.get_config_variable(
373
'api_versions'
374
).get(self._service_name, None)
375
except ProfileNotFound:
376
api_version = None
377
self._service_model = self.session.get_service_model(
378
self._service_name, api_version=api_version
379
)
380
return self._service_model
381
382
def __call__(self, args, parsed_globals):
383
# Once we know we're trying to call a service for this operation
384
# we can go ahead and create the parser for it. We
385
# can also grab the Service object from botocore.
386
service_parser = self._create_parser()
387
parsed_args, remaining = service_parser.parse_known_args(args)
388
command_table = self._get_command_table()
389
return command_table[parsed_args.operation](remaining, parsed_globals)
390
391
def _create_command_table(self):
392
command_table = OrderedDict()
393
service_model = self._get_service_model()
394
for operation_name in service_model.operation_names:
395
cli_name = xform_name(operation_name, '-')
396
operation_model = service_model.operation_model(operation_name)
397
command_table[cli_name] = ServiceOperation(
398
name=cli_name,
399
parent_name=self._name,
400
session=self.session,
401
operation_model=operation_model,
402
operation_caller=CLIOperationCaller(self.session),
403
)
404
self.session.emit(
405
f'building-command-table.{self._name}',
406
command_table=command_table,
407
session=self.session,
408
command_object=self,
409
)
410
self._add_lineage(command_table)
411
return command_table
412
413
def _add_lineage(self, command_table):
414
for command in command_table:
415
command_obj = command_table[command]
416
command_obj.lineage = self.lineage + [command_obj]
417
418
def create_help_command(self):
419
command_table = self._get_command_table()
420
return ServiceHelpCommand(
421
session=self.session,
422
obj=self._get_service_model(),
423
command_table=command_table,
424
arg_table=None,
425
event_class='.'.join(self.lineage_names),
426
name=self._name,
427
)
428
429
def _create_parser(self):
430
command_table = self._get_command_table()
431
# Also add a 'help' command.
432
command_table['help'] = self.create_help_command()
433
return ServiceArgParser(
434
operations_table=command_table, service_name=self._name
435
)
436
437
438
class ServiceOperation:
439
"""A single operation of a service.
440
441
This class represents a single operation for a service, for
442
example ``ec2.DescribeInstances``.
443
444
"""
445
446
ARG_TYPES = {
447
'list': ListArgument,
448
'boolean': BooleanArgument,
449
}
450
DEFAULT_ARG_CLASS = CLIArgument
451
452
def __init__(
453
self, name, parent_name, operation_caller, operation_model, session
454
):
455
"""
456
457
:type name: str
458
:param name: The name of the operation/subcommand.
459
460
:type parent_name: str
461
:param parent_name: The name of the parent command.
462
463
:type operation_model: ``botocore.model.OperationModel``
464
:param operation_object: The operation model
465
associated with this subcommand.
466
467
:type operation_caller: ``CLIOperationCaller``
468
:param operation_caller: An object that can properly call the
469
operation.
470
471
:type session: ``botocore.session.Session``
472
:param session: The session object.
473
474
"""
475
self._arg_table = None
476
self._name = name
477
# These is used so we can figure out what the proper event
478
# name should be <parent name>.<name>.
479
self._parent_name = parent_name
480
self._operation_caller = operation_caller
481
self._lineage = [self]
482
self._operation_model = operation_model
483
self._session = session
484
if operation_model.deprecated:
485
self._UNDOCUMENTED = True
486
487
@property
488
def name(self):
489
return self._name
490
491
@name.setter
492
def name(self, value):
493
self._name = value
494
495
@property
496
def lineage(self):
497
return self._lineage
498
499
@lineage.setter
500
def lineage(self, value):
501
self._lineage = value
502
503
@property
504
def lineage_names(self):
505
# Represents the lineage of a command in terms of command ``name``
506
return [cmd.name for cmd in self.lineage]
507
508
@property
509
def arg_table(self):
510
if self._arg_table is None:
511
self._arg_table = self._create_argument_table()
512
return self._arg_table
513
514
def __call__(self, args, parsed_globals):
515
# Once we know we're trying to call a particular operation
516
# of a service we can go ahead and load the parameters.
517
event = (
518
'before-building-argument-table-parser.'
519
f'{self._parent_name}.{self._name}'
520
)
521
self._emit(
522
event,
523
argument_table=self.arg_table,
524
args=args,
525
session=self._session,
526
parsed_globals=parsed_globals,
527
)
528
operation_parser = self._create_operation_parser(self.arg_table)
529
self._add_help(operation_parser)
530
parsed_args, remaining = operation_parser.parse_known_args(args)
531
if parsed_args.help == 'help':
532
op_help = self.create_help_command()
533
return op_help(remaining, parsed_globals)
534
elif parsed_args.help:
535
remaining.append(parsed_args.help)
536
if remaining:
537
raise UnknownArgumentError(
538
f"Unknown options: {', '.join(remaining)}"
539
)
540
event = f'operation-args-parsed.{self._parent_name}.{self._name}'
541
self._emit(
542
event, parsed_args=parsed_args, parsed_globals=parsed_globals
543
)
544
call_parameters = self._build_call_parameters(
545
parsed_args, self.arg_table
546
)
547
548
event = f'calling-command.{self._parent_name}.{self._name}'
549
override = self._emit_first_non_none_response(
550
event,
551
call_parameters=call_parameters,
552
parsed_args=parsed_args,
553
parsed_globals=parsed_globals,
554
)
555
# There are two possible values for override. It can be some type
556
# of exception that will be raised if detected or it can represent
557
# the desired return code. Note that a return code of 0 represents
558
# a success.
559
if override is not None:
560
if isinstance(override, Exception):
561
# If the override value provided back is an exception then
562
# raise the exception
563
raise override
564
else:
565
# This is the value usually returned by the ``invoke()``
566
# method of the operation caller. It represents the return
567
# code of the operation.
568
return override
569
else:
570
# No override value was supplied.
571
return self._operation_caller.invoke(
572
self._operation_model.service_model.service_name,
573
self._operation_model.name,
574
call_parameters,
575
parsed_globals,
576
)
577
578
def create_help_command(self):
579
return OperationHelpCommand(
580
self._session,
581
operation_model=self._operation_model,
582
arg_table=self.arg_table,
583
name=self._name,
584
event_class='.'.join(self.lineage_names),
585
)
586
587
def _add_help(self, parser):
588
# The 'help' output is processed a little differently from
589
# the operation help because the arg_table has
590
# CLIArguments for values.
591
parser.add_argument('help', nargs='?')
592
593
def _build_call_parameters(self, args, arg_table):
594
# We need to convert the args specified on the command
595
# line as valid **kwargs we can hand to botocore.
596
service_params = {}
597
# args is an argparse.Namespace object so we're using vars()
598
# so we can iterate over the parsed key/values.
599
parsed_args = vars(args)
600
for arg_object in arg_table.values():
601
py_name = arg_object.py_name
602
if py_name in parsed_args:
603
value = parsed_args[py_name]
604
value = self._unpack_arg(arg_object, value)
605
arg_object.add_to_params(service_params, value)
606
return service_params
607
608
def _unpack_arg(self, cli_argument, value):
609
# Unpacks a commandline argument into a Python value by firing the
610
# load-cli-arg.service-name.operation-name event.
611
session = self._session
612
service_name = self._operation_model.service_model.endpoint_prefix
613
operation_name = xform_name(self._name, '-')
614
615
return unpack_argument(
616
session, service_name, operation_name, cli_argument, value
617
)
618
619
def _create_argument_table(self):
620
argument_table = OrderedDict()
621
input_shape = self._operation_model.input_shape
622
required_arguments = []
623
arg_dict = {}
624
if input_shape is not None:
625
required_arguments = input_shape.required_members
626
arg_dict = input_shape.members
627
for arg_name, arg_shape in arg_dict.items():
628
cli_arg_name = xform_name(arg_name, '-')
629
arg_class = self.ARG_TYPES.get(
630
arg_shape.type_name, self.DEFAULT_ARG_CLASS
631
)
632
is_token = arg_shape.metadata.get('idempotencyToken', False)
633
is_required = arg_name in required_arguments and not is_token
634
event_emitter = self._session.get_component('event_emitter')
635
arg_object = arg_class(
636
name=cli_arg_name,
637
argument_model=arg_shape,
638
is_required=is_required,
639
operation_model=self._operation_model,
640
serialized_name=arg_name,
641
event_emitter=event_emitter,
642
)
643
arg_object.add_to_arg_table(argument_table)
644
LOG.debug(argument_table)
645
self._emit(
646
f'building-argument-table.{self._parent_name}.{self._name}',
647
operation_model=self._operation_model,
648
session=self._session,
649
command=self,
650
argument_table=argument_table,
651
)
652
return argument_table
653
654
def _emit(self, name, **kwargs):
655
return self._session.emit(name, **kwargs)
656
657
def _emit_first_non_none_response(self, name, **kwargs):
658
return self._session.emit_first_non_none_response(name, **kwargs)
659
660
def _create_operation_parser(self, arg_table):
661
parser = ArgTableArgParser(arg_table)
662
return parser
663
664
665
class CLIOperationCaller:
666
"""Call an AWS operation and format the response."""
667
668
def __init__(self, session):
669
self._session = session
670
671
def invoke(self, service_name, operation_name, parameters, parsed_globals):
672
"""Invoke an operation and format the response.
673
674
:type service_name: str
675
:param service_name: The name of the service. Note this is the service name,
676
not the endpoint prefix (e.g. ``ses`` not ``email``).
677
678
:type operation_name: str
679
:param operation_name: The operation name of the service. The casing
680
of the operation name should match the exact casing used by the service,
681
e.g. ``DescribeInstances``, not ``describe-instances`` or
682
``describe_instances``.
683
684
:type parameters: dict
685
:param parameters: The parameters for the operation call. Again, these values
686
have the same casing used by the service.
687
688
:type parsed_globals: Namespace
689
:param parsed_globals: The parsed globals from the command line.
690
691
:return: None, the result is displayed through a formatter, but no
692
value is returned.
693
694
"""
695
client = create_nested_client(
696
self._session,
697
service_name,
698
region_name=parsed_globals.region,
699
endpoint_url=parsed_globals.endpoint_url,
700
verify=parsed_globals.verify_ssl,
701
)
702
response = self._make_client_call(
703
client, operation_name, parameters, parsed_globals
704
)
705
self._display_response(operation_name, response, parsed_globals)
706
return 0
707
708
def _make_client_call(
709
self, client, operation_name, parameters, parsed_globals
710
):
711
py_operation_name = xform_name(operation_name)
712
if client.can_paginate(py_operation_name) and parsed_globals.paginate:
713
paginator = client.get_paginator(py_operation_name)
714
response = paginator.paginate(**parameters)
715
else:
716
response = getattr(client, xform_name(operation_name))(
717
**parameters
718
)
719
return response
720
721
def _display_response(self, command_name, response, parsed_globals):
722
output = parsed_globals.output
723
if output is None:
724
output = self._session.get_config_variable('output')
725
formatter = get_formatter(output, parsed_globals)
726
formatter(command_name, response)
727
728