summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMonty Taylor <mordred@inaugust.com>2015-11-04 09:22:17 -0500
committerMonty Taylor <mordred@inaugust.com>2015-12-06 21:39:49 -0500
commited2f34b06a7d581fb5fdd9811e3f8a7f748a2ce4 (patch)
tree39ea01ccb25f86f43fec86b66e3a1a42d9dba500
parent3ceee6118168890cb301be97575e19267afd5b22 (diff)
downloados-client-config-ed2f34b06a7d581fb5fdd9811e3f8a7f748a2ce4.tar.gz
Add method for registering argparse options
keystoneauth knows about a bunch of argparse options that users from a command line will want. We do a good job of processing them once they've been collected, but an os-client-config user doesn't have a great way to make sure that they register all of the options, especially when once considers that you really want to peek at the args to see which auth plugin has been selected so that the right arguments can be registered and displayed. Depends-On: Ifea90b981044009c3642b268dd639a703df1ef05 Change-Id: Ic196f65f89b3ccf92ebec39564f5eaefe8a4ae4b
-rw-r--r--README.rst23
-rw-r--r--os_client_config/config.py124
-rw-r--r--os_client_config/tests/test_config.py115
-rw-r--r--requirements.txt2
4 files changed, 263 insertions, 1 deletions
diff --git a/README.rst b/README.rst
index 0bdc182..d8fde59 100644
--- a/README.rst
+++ b/README.rst
@@ -296,3 +296,26 @@ Or, get all of the clouds.
cloud_config = os_client_config.OpenStackConfig().get_all_clouds()
for cloud in cloud_config:
print(cloud.name, cloud.region, cloud.config)
+
+argparse
+--------
+
+If you're using os-client-config from a program that wants to process
+command line options, there is a registration function to register the
+arguments that both os-client-config and keystoneauth know how to deal
+with - as well as a consumption argument.
+
+::
+
+ import argparse
+ import sys
+
+ import os_client_config
+
+ cloud_config = os_client_config.OpenStackConfig()
+ parser = argparse.ArgumentParser()
+ cloud_config.register_argparse_arguments(parser, sys.argv)
+
+ options = parser.parse_args()
+
+ cloud = cloud_config.get_one_cloud(argparse=options)
diff --git a/os_client_config/config.py b/os_client_config/config.py
index dff637a..48aa0e2 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -13,11 +13,14 @@
# under the License.
+# alias because we already had an option named argparse
+import argparse as argparse_mod
import json
import os
import warnings
import appdirs
+from keystoneauth1 import adapter
from keystoneauth1 import loading
import yaml
@@ -245,6 +248,9 @@ class OpenStackConfig(object):
self._cache_expiration = cache_settings.get(
'expiration', self._cache_expiration)
+ # Flag location to hold the peeked value of an argparse timeout value
+ self._argv_timeout = False
+
def _load_config_file(self):
return self._load_yaml_json_file(self._config_files)
@@ -451,6 +457,94 @@ class OpenStackConfig(object):
cloud['auth_type'] = 'password'
return cloud
+ def register_argparse_arguments(self, parser, argv, service_keys=[]):
+ """Register all of the common argparse options needed.
+
+ Given an argparse parser, register the keystoneauth Session arguments,
+ the keystoneauth Auth Plugin Options and os-cloud. Also, peek in the
+ argv to see if all of the auth plugin options should be registered
+ or merely the ones already configured.
+ :param argparse.ArgumentParser: parser to attach argparse options to
+ :param list argv: the arguments provided to the application
+ :param string service_keys: Service or list of services this argparse
+ should be specialized for, if known.
+ The first item in the list will be used
+ as the default value for service_type
+ (optional)
+
+ :raises exceptions.OpenStackConfigException if an invalid auth-type
+ is requested
+ """
+
+ local_parser = argparse_mod.ArgumentParser(add_help=False)
+
+ for p in (parser, local_parser):
+ p.add_argument(
+ '--os-cloud',
+ metavar='<name>',
+ default=os.environ.get('OS_CLOUD', None),
+ help='Named cloud to connect to')
+
+ # we need to peek to see if timeout was actually passed, since
+ # the keystoneauth declaration of it has a default, which means
+ # we have no clue if the value we get is from the ksa default
+ # for from the user passing it explicitly. We'll stash it for later
+ local_parser.add_argument('--timeout', metavar='<timeout>')
+
+ # Peek into the future and see if we have an auth-type set in
+ # config AND a cloud set, so that we know which command line
+ # arguments to register and show to the user (the user may want
+ # to say something like:
+ # openstack --os-cloud=foo --os-oidctoken=bar
+ # although I think that user is the cause of my personal pain
+ options, _args = local_parser.parse_known_args(argv)
+ if options.timeout:
+ self._argv_timeout = True
+
+ # validate = False because we're not _actually_ loading here
+ # we're only peeking, so it's the wrong time to assert that
+ # the rest of the arguments given are invalid for the plugin
+ # chosen (for instance, --help may be requested, so that the
+ # user can see what options he may want to give
+ cloud = self.get_one_cloud(argparse=options, validate=False)
+ default_auth_type = cloud.config['auth_type']
+
+ try:
+ loading.register_auth_argparse_arguments(
+ parser, argv, default=default_auth_type)
+ except Exception:
+ # Hidiing the keystoneauth exception because we're not actually
+ # loading the auth plugin at this point, so the error message
+ # from it doesn't actually make sense to os-client-config users
+ options, _args = parser.parse_known_args(argv)
+ plugin_names = loading.get_available_plugin_names()
+ raise exceptions.OpenStackConfigException(
+ "An invalid auth-type was specified: {auth_type}."
+ " Valid choices are: {plugin_names}.".format(
+ auth_type=options.os_auth_type,
+ plugin_names=",".join(plugin_names)))
+
+ if service_keys:
+ primary_service = service_keys[0]
+ else:
+ primary_service = None
+ loading.register_session_argparse_arguments(parser)
+ adapter.register_adapter_argparse_arguments(
+ parser, service_type=primary_service)
+ for service_key in service_keys:
+ # legacy clients have un-prefixed api-version options
+ parser.add_argument(
+ '--{service_key}-api-version'.format(
+ service_key=service_key.replace('_', '-'),
+ help=argparse_mod.SUPPRESS))
+ adapter.register_service_adapter_argparse_arguments(
+ parser, service_type=service_key)
+
+ # Backwards compat options for legacy clients
+ parser.add_argument('--http-timeout', help=argparse_mod.SUPPRESS)
+ parser.add_argument('--os-endpoint-type', help=argparse_mod.SUPPRESS)
+ parser.add_argument('--endpoint-type', help=argparse_mod.SUPPRESS)
+
def _fix_backwards_interface(self, cloud):
new_cloud = {}
for key in cloud.keys():
@@ -461,6 +555,30 @@ class OpenStackConfig(object):
new_cloud[target_key] = cloud[key]
return new_cloud
+ def _fix_backwards_api_timeout(self, cloud):
+ new_cloud = {}
+ # requests can only have one timeout, which means that in a single
+ # cloud there is no point in different timeout values. However,
+ # for some reason many of the legacy clients decided to shove their
+ # service name in to the arg name for reasons surpassin sanity. If
+ # we find any values that are not api_timeout, overwrite api_timeout
+ # with the value
+ service_timeout = None
+ for key in cloud.keys():
+ if key.endswith('timeout') and not (
+ key == 'timeout' or key == 'api_timeout'):
+ service_timeout = cloud[key]
+ else:
+ new_cloud[key] = cloud[key]
+ if service_timeout is not None:
+ new_cloud['api_timeout'] = service_timeout
+ # The common argparse arg from keystoneauth is called timeout, but
+ # os-client-config expects it to be called api_timeout
+ if self._argv_timeout:
+ if 'timeout' in new_cloud and new_cloud['timeout']:
+ new_cloud['api_timeout'] = new_cloud.pop('timeout')
+ return new_cloud
+
def get_all_clouds(self):
clouds = []
@@ -671,6 +789,12 @@ class OpenStackConfig(object):
else:
config[key] = val
+ # These backwards compat values are only set via argparse. If it's
+ # there, it's because it was passed in explicitly, and should win
+ config = self._fix_backwards_api_timeout(config)
+ if 'endpoint_type' in config:
+ config['interface'] = config.pop('endpoint_type')
+
for key in BOOL_KEYS:
if key in config:
if type(config[key]) is not bool:
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index aff8c6d..c9318fc 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -17,6 +17,7 @@ import copy
import os
import fixtures
+import testtools
import yaml
from os_client_config import cloud_config
@@ -341,6 +342,120 @@ class TestConfigArgparse(base.TestCase):
self.assertDictEqual({'compute_api_version': 1}, fixed_args)
+ def test_register_argparse_cloud(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ c.register_argparse_arguments(parser, [])
+ opts, _remain = parser.parse_known_args(['--os-cloud', 'foo'])
+ self.assertEqual(opts.os_cloud, 'foo')
+
+ def test_register_argparse_bad_plugin(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ self.assertRaises(
+ exceptions.OpenStackConfigException,
+ c.register_argparse_arguments,
+ parser, ['--os-auth-type', 'foo'])
+
+ def test_register_argparse_not_password(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-auth-type', 'v3token',
+ '--os-token', 'some-secret',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_token, 'some-secret')
+
+ def test_register_argparse_password(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-password', 'some-secret',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_password, 'some-secret')
+ with testtools.ExpectedException(AttributeError):
+ opts.os_token
+
+ def test_register_argparse_service_type(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-service-type', 'network',
+ '--os-endpoint-type', 'admin',
+ '--http-timeout', '20',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_service_type, 'network')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.http_timeout, '20')
+ with testtools.ExpectedException(AttributeError):
+ opts.os_network_service_type
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'network')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['api_timeout'], '20')
+ self.assertNotIn('http_timeout', cloud.config)
+
+ def test_register_argparse_network_service_type(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-endpoint-type', 'admin',
+ '--network-api-version', '4',
+ ]
+ c.register_argparse_arguments(parser, args, ['network'])
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_service_type, 'network')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.os_network_service_type, None)
+ self.assertEqual(opts.os_network_api_version, None)
+ self.assertEqual(opts.network_api_version, '4')
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'network')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['network_api_version'], '4')
+ self.assertNotIn('http_timeout', cloud.config)
+
+ def test_register_argparse_network_service_types(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-compute-service-name', 'cloudServers',
+ '--os-network-service-type', 'badtype',
+ '--os-endpoint-type', 'admin',
+ '--network-api-version', '4',
+ ]
+ c.register_argparse_arguments(
+ parser, args, ['compute', 'network', 'volume'])
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_network_service_type, 'badtype')
+ self.assertEqual(opts.os_compute_service_type, None)
+ self.assertEqual(opts.os_volume_service_type, None)
+ self.assertEqual(opts.os_service_type, 'compute')
+ self.assertEqual(opts.os_compute_service_name, 'cloudServers')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.os_network_api_version, None)
+ self.assertEqual(opts.network_api_version, '4')
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'compute')
+ self.assertEqual(cloud.config['network_service_type'], 'badtype')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['network_api_version'], '4')
+ self.assertNotIn('volume_service_type', cloud.config)
+ self.assertNotIn('http_timeout', cloud.config)
+
class TestConfigDefault(base.TestCase):
diff --git a/requirements.txt b/requirements.txt
index 3c32ced..1531be8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,5 @@
# process, which may cause wedges in the gate later.
PyYAML>=3.1.0
appdirs>=1.3.0
-keystoneauth1>=1.0.0
+keystoneauth1>=2.1.0
requestsexceptions>=1.1.1 # Apache-2.0