Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add an 'aws configure list' command #513

Merged
merged 4 commits into from

3 participants

@jamesls
Owner

This will show you where all the config values are coming from.
As part of this change, I also updated the commands.py module
to support subcommands. This makes it easy to create a top
level command that supports subcommands in a declarative manner.
You can now run:

  • aws configure
  • aws configure list

As an example, you'll see output like this:

        Name                    Value             Type    Location
        ----                    -----             ----    --------
     profile                <not set>             None    None
  access_key     ****************ABCD      config_file    ~/.aws/config
  secret_key     ****************ABCD      config_file    ~/.aws/config
      region                us-west-2      config_file    ~/.aws/config
jamesls added some commits
@jamesls jamesls Remove unused function a0fa3ec
@jamesls jamesls Add an 'aws configure list' command
This will show you where all the config values are coming from.
As part of this change, I also updated the commands.py module
to support subcommands.  This makes it easy to create a top
level command that supports subcommands in a declarative manner.
You can now run:

* `aws configure`
* `aws configure list`

As an example, you'll see output like this:

        Name                    Value             Type    Location
        ----                    -----             ----    --------
     profile                <not set>             None    None
  access_key     ****************ABCD      config_file    ~/.aws/config
  secret_key     ****************ABCD      config_file    ~/.aws/config
      region                us-west-2      config_file    ~/.aws/config
a54f886
@jamesls jamesls Add docs for the aws configure list command 83574d2
awscli/argparser.py
@@ -94,18 +94,26 @@ class ArgTableArgParser(CLIArgParser):
"""CLI arg parser based on an argument table."""
Usage = ("aws [options] <command> <subcommand> [parameters]")
- def __init__(self, argument_table):
+ def __init__(self, argument_table, command_table=None):
+ # command_table is an optional subcommand_table. If it's passed
+ # in, then we'l update the argparse to parse a 'subcommand' argument

"we'l" -> "we'll"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@toastdriven

That comment could be fixed whenever, doesn't have to be part of this. LGTM. :ship: it.

@garnaat

LGTM

@jamesls jamesls merged commit 89c1551 into aws:develop

1 check passed

Details default The Travis CI build passed
@jamesls jamesls deleted the jamesls:configure-list branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 26, 2013
  1. @jamesls

    Remove unused function

    jamesls authored
  2. @jamesls

    Add an 'aws configure list' command

    jamesls authored
    This will show you where all the config values are coming from.
    As part of this change, I also updated the commands.py module
    to support subcommands.  This makes it easy to create a top
    level command that supports subcommands in a declarative manner.
    You can now run:
    
    * `aws configure`
    * `aws configure list`
    
    As an example, you'll see output like this:
    
            Name                    Value             Type    Location
            ----                    -----             ----    --------
         profile                <not set>             None    None
      access_key     ****************ABCD      config_file    ~/.aws/config
      secret_key     ****************ABCD      config_file    ~/.aws/config
          region                us-west-2      config_file    ~/.aws/config
  3. @jamesls
  4. @jamesls
This page is out of date. Refresh to see the latest.
View
14 awscli/argparser.py
@@ -94,18 +94,26 @@ class ArgTableArgParser(CLIArgParser):
"""CLI arg parser based on an argument table."""
Usage = ("aws [options] <command> <subcommand> [parameters]")
- def __init__(self, argument_table):
+ def __init__(self, argument_table, command_table=None):
+ # command_table is an optional subcommand_table. If it's passed
+ # in, then we'll update the argparse to parse a 'subcommand' argument
+ # and populate the choices field with the command table keys.
super(ArgTableArgParser, self).__init__(
formatter_class=self.Formatter,
add_help=False,
usage=self.Usage,
conflict_handler='resolve')
- self._build(argument_table)
+ if command_table is None:
+ command_table = {}
+ self._build(argument_table, command_table)
- def _build(self, argument_table):
+ def _build(self, argument_table, command_table):
for arg_name in argument_table:
argument = argument_table[arg_name]
argument.add_to_parser(self)
+ if command_table:
+ self.add_argument('subcommand', choices=list(command_table.keys()),
+ nargs='?')
def parse_known_args(self, args, namespace=None):
if len(args) == 1 and args[0] == 'help':
View
35 awscli/customizations/commands.py
@@ -1,5 +1,7 @@
import bcdoc.docevents
+from botocore.compat import OrderedDict
+
from awscli.clidocs import CLIDocumentEventHandler
from awscli.argparser import ArgTableArgParser
from awscli.clidriver import CLICommand
@@ -43,6 +45,15 @@ class BasicCommand(CLICommand):
# 'action': 'store', 'choices': ['a', 'b', 'c']},
# ]
ARG_TABLE = []
+ # If you want the command to have subcommands, you can provide a list of
+ # dicts. We use a list here because we want to allow a user to provide
+ # the order they want to use for subcommands.
+ # SUBCOMMANDS = [
+ # {'name': 'subcommand1', 'command_class': SubcommandClass},
+ # {'name': 'subcommand2', 'command_class': SubcommandClass2},
+ # ]
+ # The command_class must subclass from ``BasicCommand``.
+ SUBCOMMANDS = []
# At this point, the only other thing you have to implement is a _run_main
# method (see the method for more information).
@@ -54,12 +65,17 @@ def __call__(self, args, parsed_globals):
# args is the remaining unparsed args.
# We might be able to parse these args so we need to create
# an arg parser and parse them.
- parser = ArgTableArgParser(self.arg_table)
- parsed_args = parser.parse_args(args)
+ subcommand_table = self._build_subcommand_table()
+ parser = ArgTableArgParser(self.arg_table, subcommand_table)
+ parsed_args, remaining = parser.parse_known_args(args)
if hasattr(parsed_args, 'help'):
self._display_help(parsed_args, parsed_globals)
- else:
+ elif getattr(parsed_args, 'subcommand', None) is None:
+ # No subcommand was specified so call the main
+ # function for this top level command.
self._run_main(parsed_args, parsed_globals)
+ else:
+ subcommand_table[parsed_args.subcommand](remaining, parsed_globals)
def _run_main(self, parsed_args, parsed_globals):
# Subclasses should implement this method.
@@ -70,7 +86,18 @@ def _run_main(self, parsed_args, parsed_globals):
# provided as the 'dest' key. Otherwise they default to the
# 'name' key. For example: ARG_TABLE[0] = {"name": "foo-arg", ...}
# can be accessed by ``parsed_args.foo_arg``.
- raise NotImlementedError("_run_main")
+ raise NotImplementedError("_run_main")
+
+ def _build_subcommand_table(self):
+ subcommand_table = OrderedDict()
+ for subcommand in self.SUBCOMMANDS:
+ subcommand_name = subcommand['name']
+ subcommand_class = subcommand['command_class']
+ subcommand_table[subcommand_name] = subcommand_class(self._session)
+ self._session.emit('building-command-table.%s' % self.NAME,
+ command_table=subcommand_table,
+ session=self._session)
+ return subcommand_table
def _display_help(self, parsed_args, parsed_globals):
help_command = self.create_help_command()
View
135 awscli/customizations/configure.py
@@ -12,6 +12,7 @@
# language governing permissions and limitations under the License.
import os
import re
+import sys
import logging
import six
@@ -27,24 +28,41 @@
logger = logging.getLogger(__name__)
+NOT_SET = '<not set>'
def register_configure_cmd(cli):
cli.register('building-command-table.main',
ConfigureCommand.add_command)
-def add_configure_cmd(command_table, session, **kwargs):
- command_table['configure'] = ConfigureCommand(session)
+
+class ConfigValue(object):
+ def __init__(self, value, config_type, config_variable):
+ self.value = value
+ self.config_type = config_type
+ self.config_variable = config_variable
+
+ def mask_value(self):
+ if self.value is NOT_SET:
+ return
+ self.value = _mask_value(self.value)
class SectionNotFoundError(Exception):
pass
+def _mask_value(current_value):
+ if current_value is None:
+ return 'None'
+ else:
+ return ('*' * 16) + current_value[-4:]
+
+
class InteractivePrompter(object):
def get_value(self, current_value, config_name, prompt_text=''):
if config_name in ('aws_access_key_id', 'aws_secret_access_key'):
- current_value = self._mask_value(current_value)
+ current_value = _mask_value(current_value)
response = raw_input("%s [%s]: " % (prompt_text, current_value))
if not response:
# If the user hits enter, we return a value of None
@@ -53,12 +71,6 @@ def get_value(self, current_value, config_name, prompt_text=''):
response = None
return response
- def _mask_value(self, current_value):
- if current_value is None:
- return 'None'
- else:
- return ('*' * 16) + current_value[-4:]
-
class ConfigFileWriter(object):
SECTION_REGEX = re.compile(r'\[(?P<header>[^]]+)\]')
@@ -157,6 +169,108 @@ def _matches_section(self, match, section_name):
return unquoted_match
+class ConfigureListCommand(BasicCommand):
+ NAME = 'list'
+ DESCRIPTION = (
+ 'List the AWS CLI configuration data. This command will '
+ 'show you the current configuration data. For each configuration '
+ 'item, it will show you the value, where the configuration value '
+ 'was retrieved, and the configuration variable name. For example, '
+ 'if you provide the AWS region in an environment variable, this '
+ 'command will show you the name of the region you\'ve configured, '
+ 'it will tell you that this value came from an environment '
+ 'variable, and it will tell you the name of the environment '
+ 'variable.\n'
+ )
+ SYNOPSIS = ('aws configure list [--profile profile-name]')
+ EXAMPLES = (
+ 'To show your current configuration values::\n'
+ '\n'
+ ' $ aws configure list\n'
+ ' Name Value Type Location\n'
+ ' ---- ----- ---- --------\n'
+ ' profile <not set> None None\n'
+ ' access_key ****************ABCD config_file ~/.aws/config\n'
+ ' secret_key ****************ABCD config_file ~/.aws/config\n'
+ ' region us-west-2 env AWS_DEFAULT_REGION\n'
+ '\n'
+ )
+
+ def __init__(self, session, stream=sys.stdout):
+ super(ConfigureListCommand, self).__init__(session)
+ self._stream = stream
+
+ def _run_main(self, args, parsed_globals):
+ self._display_config_value(ConfigValue('Value', 'Type', 'Location'),
+ 'Name')
+ self._display_config_value(ConfigValue('-----', '----', '--------'),
+ '----')
+
+ if self._session.profile is not None:
+ profile = ConfigValue(self._session.profile, 'manual',
+ '--profile')
+ else:
+ profile = self._lookup_config('profile')
+ self._display_config_value(profile, 'profile')
+
+ access_key, secret_key = self._lookup_credentials()
+ self._display_config_value(access_key, 'access_key')
+ self._display_config_value(secret_key, 'secret_key')
+
+ region = self._lookup_config('region')
+ self._display_config_value(region, 'region')
+
+ def _display_config_value(self, config_value, config_name):
+ self._stream.write('%10s %24s %16s %s\n' % (
+ config_name, config_value.value, config_value.config_type,
+ config_value.config_variable))
+
+ def _lookup_credentials(self):
+ # First try it with _lookup_config. It's possible
+ # that we don't find credentials this way (for example,
+ # if we're using an IAM role).
+ access_key = self._lookup_config('access_key')
+ if access_key.value is not NOT_SET:
+ secret_key = self._lookup_config('secret_key')
+ access_key.mask_value()
+ secret_key.mask_value()
+ return access_key, secret_key
+ else:
+ # Otherwise we can try to use get_credentials().
+ # This includes a few more lookup locations
+ # (IAM roles, some of the legacy configs, etc.)
+ credentials = self._session.get_credentials()
+ if credentials is None:
+ no_config = ConfigValue(NOT_SET, None, None)
+ return no_config, no_config
+ else:
+ # For the ConfigValue, we don't track down the
+ # config_variable because that info is not
+ # visible from botocore.credentials. I think
+ # the credentials.method is sufficient to show
+ # where the credentials are coming from.
+ access_key = ConfigValue(credentials.access_key,
+ credentials.method, '')
+ secret_key = ConfigValue(credentials.secret_key,
+ credentials.method, '')
+ access_key.mask_value()
+ secret_key.mask_value()
+ return access_key, secret_key
+
+ def _lookup_config(self, name):
+ # First try to look up the variable in the env.
+ value = self._session.get_variable(name, methods=('env',))
+ if value is not None:
+ return ConfigValue(value, 'env', self._session.env_vars[name][1])
+ # Then try to look up the variable in the config file.
+ value = self._session.get_variable(name, methods=('config',))
+ if value is not None:
+ return ConfigValue(value, 'config_file',
+ self._session.get_variable('config_file'))
+ else:
+ return ConfigValue(NOT_SET, None, None)
+
+
class ConfigureCommand(BasicCommand):
NAME = 'configure'
DESCRIPTION = (
@@ -192,6 +306,9 @@ class ConfigureCommand(BasicCommand):
' Default region name [us-west-1]: us-west-2\n'
' Default output format [None]:\n'
)
+ SUBCOMMANDS = [
+ {'name': 'list', 'command_class': ConfigureListCommand}
+ ]
# If you want to add new values to prompt, update this list here.
VALUES_TO_PROMPT = [
View
115 tests/unit/customizations/test_configure.py
@@ -18,6 +18,7 @@
from tests import BaseAWSHelpOutputTest
import mock
+from six import StringIO
from botocore.exceptions import ProfileNotFound
from awscli.customizations import configure
@@ -46,10 +47,23 @@ def get_value(self, current_value, config_name, prompt_text=''):
class FakeSession(object):
- def __init__(self, variables, profile_does_not_exist=False):
- self.variables = variables
+ def __init__(self, all_variables, profile_does_not_exist=False,
+ config_file_vars=None, environment_vars=None,
+ credentials=None):
+ self.variables = all_variables
self.profile_does_not_exist = profile_does_not_exist
self.config = {}
+ if config_file_vars is None:
+ config_file_vars = {}
+ self.config_file_vars = config_file_vars
+ if environment_vars is None:
+ environment_vars = {}
+ self.environment_vars = environment_vars
+ self._credentials = credentials
+ self.profile = None
+
+ def get_credentials(self):
+ return self._credentials
def get_config(self):
if self.profile_does_not_exist:
@@ -59,7 +73,16 @@ def get_config(self):
def get_variable(self, name, methods=None):
if self.profile_does_not_exist and not name == 'config_file':
raise ProfileNotFound(profile='foo')
- return self.variables.get(name)
+ if methods is not None:
+ if 'env' in methods:
+ return self.environment_vars.get(name)
+ elif 'config' in methods:
+ return self.config_file_vars.get(name)
+ else:
+ return self.variables.get(name)
+
+ def emit(self, event_name, **kwargs):
+ pass
class TestConfigureCommand(unittest.TestCase):
@@ -422,3 +445,89 @@ def test_profile_with_multiple_spaces(self):
'foo = newvalue\n'
'bar = 1\n'
)
+
+
+class TestConfigureListCommand(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_configure_list_command_nothing_set(self):
+ # Test the case where the user only wants to change a single_value.
+ session = FakeSession(all_variables={'config_file': '/config/location'})
+ stream = StringIO()
+ self.configure_list = configure.ConfigureListCommand(session, stream)
+ self.configure_list(args=[], parsed_globals=None)
+ rendered = stream.getvalue()
+ self.assertRegexpMatches(rendered, 'profile\s+<not set>')
+ self.assertRegexpMatches(rendered, 'access_key\s+<not set>')
+ self.assertRegexpMatches(rendered, 'secret_key\s+<not set>')
+ self.assertRegexpMatches(rendered, 'region\s+<not set>')
+
+ def test_configure_from_env(self):
+ env_vars = {
+ 'profile': 'myprofilename'
+ }
+ session = FakeSession(
+ all_variables={'config_file': '/config/location'},
+ environment_vars=env_vars)
+ session.env_vars = {'profile': (None, "PROFILE_ENV_VAR")}
+ stream = StringIO()
+ self.configure_list = configure.ConfigureListCommand(session, stream)
+ self.configure_list(args=[], parsed_globals=None)
+ rendered = stream.getvalue()
+ self.assertRegexpMatches(
+ rendered, 'profile\s+myprofilename\s+env\s+PROFILE_ENV_VAR')
+
+ def test_configure_from_config_file(self):
+ config_file_vars = {
+ 'region': 'us-west-2'
+ }
+ session = FakeSession(
+ all_variables={'config_file': '/config/location'},
+ config_file_vars=config_file_vars)
+ session.env_vars = {'region': ('region', "AWS_REGION")}
+ stream = StringIO()
+ self.configure_list = configure.ConfigureListCommand(session, stream)
+ self.configure_list(args=[], parsed_globals=None)
+ rendered = stream.getvalue()
+ self.assertRegexpMatches(
+ rendered, 'region\s+us-west-2\s+config_file\s+/config/location')
+
+ def test_configure_from_multiple_sources(self):
+ # Here the profile is from an env var, the
+ # region is from the config file, and the credentials
+ # are from an iam-role.
+ env_vars = {
+ 'profile': 'myprofilename'
+ }
+ config_file_vars = {
+ 'region': 'us-west-2'
+ }
+ credentials = mock.Mock()
+ credentials.access_key = 'access_key'
+ credentials.secret_key = 'secret_key'
+ credentials.method = 'iam-role'
+ session = FakeSession(
+ all_variables={'config_file': '/config/location'},
+ environment_vars=env_vars,
+ config_file_vars=config_file_vars,
+ credentials=credentials)
+ session.env_vars = {'region': ('region', 'AWS_REGION'),
+ 'profile': ('profile', 'AWS_DEFAULT_PROFILE')}
+ stream = StringIO()
+ self.configure_list = configure.ConfigureListCommand(session, stream)
+ self.configure_list(args=[], parsed_globals=None)
+ rendered = stream.getvalue()
+ # The profile came from an env var.
+ self.assertRegexpMatches(
+ rendered, 'profile\s+myprofilename\s+env\s+AWS_DEFAULT_PROFILE')
+ # The region came from the config file.
+ self.assertRegexpMatches(
+ rendered, 'region\s+us-west-2\s+config_file\s+/config/location')
+ # The credentials came from an IAM role. Note how we're
+ # also checking that the access_key/secret_key are masked
+ # with '*' chars except for the last 4 chars.
+ self.assertRegexpMatches(
+ rendered, r'access_key\s+\*+_key\s+iam-role')
+ self.assertRegexpMatches(
+ rendered, r'secret_key\s+\*+_key\s+iam-role')
Something went wrong with that request. Please try again.