From b17bbcdef9acfaf6e2b47671c9982d3378045961 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sun, 22 Nov 2015 13:16:44 -0500
Subject: [PATCH 01/39] Add support for secure.yaml file for auth info

Almost nothing in clouds.yaml is secret, but the file has to be treated
as if it were because of the passwords or other secrets contained in it.
This makes it difficult to put clouds.yaml into a public or broadly
accessible config repository. Add support for having a second optional
file, secure.yaml, which can contain any value you can put in
clouds.yaml and which will be overlayed on top of clouds.yaml values.

Most people probably do not need this, but for folks with complex cloud
configs with teams of people working on them, this reduces the amount of
things that have to be managed by the privileged system.

Change-Id: I631d826588b0a0b1f36244caa7982dd42d9eb498
---
 README.rst                             | 28 ++++++++++++++++++++++
 os_client_config/config.py             | 33 +++++++++++++++++++++++++-
 os_client_config/tests/base.py         | 12 +++++++++-
 os_client_config/tests/test_config.py  | 21 ++++++++++------
 os_client_config/tests/test_environ.py | 14 +++++++----
 5 files changed, 95 insertions(+), 13 deletions(-)

diff --git a/README.rst b/README.rst
index f16bbc0..0bdc182 100644
--- a/README.rst
+++ b/README.rst
@@ -145,6 +145,34 @@ as a result of a chosen plugin need to go into the auth dict. For password
 auth, this includes `auth_url`, `username` and `password` as well as anything
 related to domains, projects and trusts.
 
+Splitting Secrets
+-----------------
+
+In some scenarios, such as configuragtion managment controlled environments,
+it might be eaiser to have secrets in one file and non-secrets in another.
+This is fully supported via an optional file `secure.yaml` which follows all
+the same location rules as `clouds.yaml`. It can contain anything you put
+in `clouds.yaml` and will take precedence over anything in the `clouds.yaml`
+file.
+
+::
+
+  # clouds.yaml
+  clouds:
+    internap:
+      profile: internap
+      auth:
+        username: api-55f9a00fb2619
+        project_name: inap-17037
+      regions:
+      - ams01
+      - nyj01
+  # secure.yaml
+  clouds:
+    internap:
+      auth:
+        password: XXXXXXXXXXXXXXXXX
+
 SSL Settings
 ------------
 
diff --git a/os_client_config/config.py b/os_client_config/config.py
index 5f7c402..c12b25a 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -51,6 +51,11 @@ CONFIG_FILES = [
     for d in CONFIG_SEARCH_PATH
     for s in YAML_SUFFIXES + JSON_SUFFIXES
 ]
+SECURE_FILES = [
+    os.path.join(d, 'secure' + s)
+    for d in CONFIG_SEARCH_PATH
+    for s in YAML_SUFFIXES + JSON_SUFFIXES
+]
 VENDOR_FILES = [
     os.path.join(d, 'clouds-public' + s)
     for d in CONFIG_SEARCH_PATH
@@ -102,6 +107,20 @@ def _get_os_environ(envvar_prefix=None):
     return ret
 
 
+def _merge_clouds(old_dict, new_dict):
+    """Like dict.update, except handling nested dicts."""
+    ret = old_dict.copy()
+    for (k, v) in new_dict.items():
+        if isinstance(v, dict):
+            if k in ret:
+                ret[k] = _merge_clouds(ret[k], v)
+            else:
+                ret[k] = v.copy()
+        else:
+            ret[k] = v
+    return ret
+
+
 def _auth_update(old_dict, new_dict):
     """Like dict.update, except handling the nested dict called auth."""
     for (k, v) in new_dict.items():
@@ -119,20 +138,29 @@ class OpenStackConfig(object):
 
     def __init__(self, config_files=None, vendor_files=None,
                  override_defaults=None, force_ipv4=None,
-                 envvar_prefix=None):
+                 envvar_prefix=None, secure_files=None):
         self._config_files = config_files or CONFIG_FILES
+        self._secure_files = secure_files or SECURE_FILES
         self._vendor_files = vendor_files or VENDOR_FILES
 
         config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None)
         if config_file_override:
             self._config_files.insert(0, config_file_override)
 
+        secure_file_override = os.environ.pop('OS_CLIENT_SECURE_FILE', None)
+        if secure_file_override:
+            self._secure_files.insert(0, secure_file_override)
+
         self.defaults = defaults.get_defaults()
         if override_defaults:
             self.defaults.update(override_defaults)
 
         # First, use a config file if it exists where expected
         self.config_filename, self.cloud_config = self._load_config_file()
+        _, secure_config = self._load_secure_file()
+        if secure_config:
+            self.cloud_config = _merge_clouds(
+                self.cloud_config, secure_config)
 
         if not self.cloud_config:
             self.cloud_config = {'clouds': {}}
@@ -220,6 +248,9 @@ class OpenStackConfig(object):
     def _load_config_file(self):
         return self._load_yaml_json_file(self._config_files)
 
+    def _load_secure_file(self):
+        return self._load_yaml_json_file(self._secure_files)
+
     def _load_vendor_file(self):
         return self._load_yaml_json_file(self._vendor_files)
 
diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py
index 3d94e25..33a868d 100644
--- a/os_client_config/tests/base.py
+++ b/os_client_config/tests/base.py
@@ -64,7 +64,6 @@ USER_CONF = {
             'auth': {
                 'auth_url': 'http://example.com/v2',
                 'username': 'testuser',
-                'password': 'testpass',
                 'project_name': 'testproject',
             },
             'region-name': 'test-region',
@@ -112,6 +111,15 @@ USER_CONF = {
         }
     },
 }
+SECURE_CONF = {
+    'clouds': {
+        '_test_cloud_no_vendor': {
+            'auth': {
+                'password': 'testpass',
+            },
+        }
+    }
+}
 NO_CONF = {
     'cache': {'max_age': 1},
 }
@@ -135,6 +143,7 @@ class TestCase(base.BaseTestCase):
         tdir = self.useFixture(fixtures.TempDir())
         conf['cache']['path'] = tdir.path
         self.cloud_yaml = _write_yaml(conf)
+        self.secure_yaml = _write_yaml(SECURE_CONF)
         self.vendor_yaml = _write_yaml(VENDOR_CONF)
         self.no_yaml = _write_yaml(NO_CONF)
 
@@ -155,6 +164,7 @@ class TestCase(base.BaseTestCase):
         self.assertIsNone(cc.cloud)
         self.assertIn('username', cc.auth)
         self.assertEqual('testuser', cc.auth['username'])
+        self.assertEqual('testpass', cc.auth['password'])
         self.assertFalse(cc.config['image_api_use_tasks'])
         self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth)
         if 'project_name' in cc.auth:
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index d225b7c..aff8c6d 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -30,7 +30,8 @@ class TestConfig(base.TestCase):
 
     def test_get_all_clouds(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
-                                   vendor_files=[self.vendor_yaml])
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
         clouds = c.get_all_clouds()
         # We add one by hand because the regions cloud is going to exist
         # twice since it has two regions in it
@@ -74,7 +75,8 @@ class TestConfig(base.TestCase):
 
     def test_get_one_cloud_with_config_files(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
-                                   vendor_files=[self.vendor_yaml])
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.secure_yaml])
         self.assertIsInstance(c.cloud_config, dict)
         self.assertIn('cache', c.cloud_config)
         self.assertIsInstance(c.cloud_config['cache'], dict)
@@ -129,7 +131,8 @@ class TestConfig(base.TestCase):
 
     def test_fallthrough(self):
         c = config.OpenStackConfig(config_files=[self.no_yaml],
-                                   vendor_files=[self.no_yaml])
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
         for k in os.environ.keys():
             if k.startswith('OS_'):
                 self.useFixture(fixtures.EnvironmentVariable(k))
@@ -137,7 +140,8 @@ class TestConfig(base.TestCase):
 
     def test_prefer_ipv6_true(self):
         c = config.OpenStackConfig(config_files=[self.no_yaml],
-                                   vendor_files=[self.no_yaml])
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
         cc = c.get_one_cloud(cloud='defaults', validate=False)
         self.assertTrue(cc.prefer_ipv6)
 
@@ -155,7 +159,8 @@ class TestConfig(base.TestCase):
 
     def test_force_ipv4_false(self):
         c = config.OpenStackConfig(config_files=[self.no_yaml],
-                                   vendor_files=[self.no_yaml])
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
         cc = c.get_one_cloud(cloud='defaults', validate=False)
         self.assertFalse(cc.force_ipv4)
 
@@ -166,7 +171,8 @@ class TestConfig(base.TestCase):
         self.assertEqual('testpass', cc.auth['password'])
 
     def test_get_cloud_names(self):
-        c = config.OpenStackConfig(config_files=[self.cloud_yaml])
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   secure_files=[self.no_yaml])
         self.assertEqual(
             ['_test-cloud-domain-id_',
              '_test-cloud-int-project_',
@@ -177,7 +183,8 @@ class TestConfig(base.TestCase):
              ],
             sorted(c.get_cloud_names()))
         c = config.OpenStackConfig(config_files=[self.no_yaml],
-                                   vendor_files=[self.no_yaml])
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
         for k in os.environ.keys():
             if k.startswith('OS_'):
                 self.useFixture(fixtures.EnvironmentVariable(k))
diff --git a/os_client_config/tests/test_environ.py b/os_client_config/tests/test_environ.py
index 0ff800f..b75db1c 100644
--- a/os_client_config/tests/test_environ.py
+++ b/os_client_config/tests/test_environ.py
@@ -29,6 +29,8 @@ class TestEnviron(base.TestCase):
             fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com'))
         self.useFixture(
             fixtures.EnvironmentVariable('OS_USERNAME', 'testuser'))
+        self.useFixture(
+            fixtures.EnvironmentVariable('OS_PASSWORD', 'testpass'))
         self.useFixture(
             fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject'))
         self.useFixture(
@@ -57,13 +59,15 @@ class TestEnviron(base.TestCase):
         self.useFixture(
             fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false'))
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
-                                   vendor_files=[self.vendor_yaml])
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.secure_yaml])
         cc = c.get_one_cloud('_test-cloud_')
         self.assertFalse(cc.prefer_ipv6)
 
     def test_environ_exists(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
-                                   vendor_files=[self.vendor_yaml])
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.secure_yaml])
         cc = c.get_one_cloud('envvars')
         self._assert_cloud_details(cc)
         self.assertNotIn('auth_url', cc.config)
@@ -78,7 +82,8 @@ class TestEnviron(base.TestCase):
     def test_environ_prefix(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml],
-                                   envvar_prefix='NOVA_')
+                                   envvar_prefix='NOVA_',
+                                   secure_files=[self.secure_yaml])
         cc = c.get_one_cloud('envvars')
         self._assert_cloud_details(cc)
         self.assertNotIn('auth_url', cc.config)
@@ -92,7 +97,8 @@ class TestEnviron(base.TestCase):
 
     def test_get_one_cloud_with_config_files(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
-                                   vendor_files=[self.vendor_yaml])
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.secure_yaml])
         self.assertIsInstance(c.cloud_config, dict)
         self.assertIn('cache', c.cloud_config)
         self.assertIsInstance(c.cloud_config['cache'], dict)

From 026a17c9eb9d8ebad8c56f8d1b7946bd4694519e Mon Sep 17 00:00:00 2001
From: Jamie Lennox <jamielennox@gmail.com>
Date: Wed, 2 Dec 2015 11:05:29 +0800
Subject: [PATCH 02/39] Remove optional keystoneauth1 imports

keystoneauth1 is now a hard dependency of os-client-config so there is
no way that this should not be importable.

Change-Id: I20901623e8b29f50d7ab1ed956472a4b1eda51bf
---
 os_client_config/config.py | 37 +++++++++++++++----------------------
 1 file changed, 15 insertions(+), 22 deletions(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 5f7c402..19cfa35 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -18,10 +18,7 @@ import os
 import warnings
 
 import appdirs
-try:
-    from keystoneauth1 import loading
-except ImportError:
-    loading = None
+from keystoneauth1 import loading
 import yaml
 
 from os_client_config import cloud_config
@@ -657,27 +654,23 @@ class OpenStackConfig(object):
         #                compatible behaviour
         config = self.auth_config_hook(config)
 
-        if loading:
-            if validate:
-                try:
-                    loader = self._get_auth_loader(config)
-                    config = self._validate_auth(config, loader)
-                    auth_plugin = loader.load_from_options(**config['auth'])
-                except Exception as e:
-                    # We WANT the ksa exception normally
-                    # but OSC can't handle it right now, so we try deferring
-                    # to ksc. If that ALSO fails, it means there is likely
-                    # a deeper issue, so we assume the ksa error was correct
-                    auth_plugin = None
-                    try:
-                        config = self._validate_auth_ksc(config)
-                    except Exception:
-                        raise e
-            else:
+        if validate:
+            try:
+                loader = self._get_auth_loader(config)
+                config = self._validate_auth(config, loader)
+                auth_plugin = loader.load_from_options(**config['auth'])
+            except Exception as e:
+                # We WANT the ksa exception normally
+                # but OSC can't handle it right now, so we try deferring
+                # to ksc. If that ALSO fails, it means there is likely
+                # a deeper issue, so we assume the ksa error was correct
                 auth_plugin = None
+                try:
+                    config = self._validate_auth_ksc(config)
+                except Exception:
+                    raise e
         else:
             auth_plugin = None
-            config = self._validate_auth_ksc(config)
 
         # If any of the defaults reference other values, we need to expand
         for (key, value) in config.items():

From eea460d5917a1536025d66ce3e2b3f9094d46e7c Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Thu, 3 Dec 2015 07:34:23 -0800
Subject: [PATCH 03/39] Make sure that cloud always has a name

If we don't ask for a cloud, and we fall through to the envvars cloud or
the defaults cloud, the cloud that is returned winds up not being named
- even though we know what cloud it is. Set the name of the cloud we're
working with.

This is important for the next patch, where we need to peek at the
config to get some default values, but in a fallthrough case we do not
know which cloud to request.

Change-Id: Ie56e490d4384f2d680450bc956e4b7b5b8099f0e
---
 os_client_config/config.py | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 19cfa35..ea3f6a1 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -177,6 +177,8 @@ class OpenStackConfig(object):
         envvars = _get_os_environ(envvar_prefix=envvar_prefix)
         if envvars:
             self.cloud_config['clouds'][self.envvar_key] = envvars
+            if not self.default_cloud:
+                self.default_cloud = self.envvar_key
 
         # Finally, fall through and make a cloud that starts with defaults
         # because we need somewhere to put arguments, and there are neither
@@ -184,6 +186,7 @@ class OpenStackConfig(object):
         if not self.cloud_config['clouds']:
             self.cloud_config = dict(
                 clouds=dict(defaults=dict(self.defaults)))
+            self.default_cloud = 'defaults'
 
         self._cache_expiration_time = 0
         self._cache_path = CACHE_PATH
@@ -604,14 +607,14 @@ class OpenStackConfig(object):
             on missing required auth parameters
         """
 
-        if cloud is None and self.default_cloud:
-            cloud = self.default_cloud
-
-        if cloud is None and self.envvar_key in self.get_cloud_names():
-            cloud = self.envvar_key
-
         args = self._fix_args(kwargs, argparse=argparse)
 
+        if cloud is None:
+            if 'cloud' in args:
+                cloud = args['cloud']
+            else:
+                cloud = self.default_cloud
+
         if 'region_name' not in args or args['region_name'] is None:
             args['region_name'] = self._get_region(cloud)
 

From 6aecb87e7f2cb2c031bf9c89564ea0b46d387c79 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sun, 6 Dec 2015 10:35:08 -0500
Subject: [PATCH 04/39] Update vexxhost to Identity v3

There is a discovery URL for vexxhost for keystone v3. Also, there is a
new vexxhost domain for it. Also, vexxhost has DNS running designate v1.
And make the region list a list of one region, because there is a second
region coming soon.

Change-Id: Ie72c19976646f41c713124659e69725df59e1580
---
 doc/source/vendor-support.rst          | 5 ++++-
 os_client_config/vendors/vexxhost.json | 8 ++++++--
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst
index 90fd31f..d7af6b9 100644
--- a/doc/source/vendor-support.rst
+++ b/doc/source/vendor-support.rst
@@ -271,10 +271,13 @@ gd1            Guangdong
 vexxhost
 --------
 
-http://auth.api.thenebulacloud.com:5000/v2.0/
+http://auth.vexxhost.net
 
 ============== ================
 Region Name    Human Name
 ============== ================
 ca-ymq-1       Montreal
 ============== ================
+
+* DNS API Version is 1
+* Identity API Version is 3
diff --git a/os_client_config/vendors/vexxhost.json b/os_client_config/vendors/vexxhost.json
index 25911ca..dd683be 100644
--- a/os_client_config/vendors/vexxhost.json
+++ b/os_client_config/vendors/vexxhost.json
@@ -2,9 +2,13 @@
   "name": "vexxhost",
   "profile": {
     "auth": {
-      "auth_url": "http://auth.api.thenebulacloud.com:5000/v2.0/"
+      "auth_url": "http://auth.vexxhost.net"
     },
-    "region_name": "ca-ymq-1",
+    "regions": [
+      "ca-ymq-1"
+    ],
+    "dns_api_version": "1",
+    "identity_api_version": "3",
     "floating_ip_source": "None"
   }
 }

From ed2f34b06a7d581fb5fdd9811e3f8a7f748a2ce4 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 4 Nov 2015 09:22:17 -0500
Subject: [PATCH 05/39] 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
---
 README.rst                            |  23 +++++
 os_client_config/config.py            | 124 ++++++++++++++++++++++++++
 os_client_config/tests/test_config.py | 115 ++++++++++++++++++++++++
 requirements.txt                      |   2 +-
 4 files changed, 263 insertions(+), 1 deletion(-)

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

From 5beaeef2c3140f84b2e5a57a789460d4db9ff766 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Thu, 3 Dec 2015 11:26:12 -0600
Subject: [PATCH 06/39] Add simple helper function for client construction

Often times you don't want to take advantage of all the flexibility, you
simple want the basic works-like-it-should thing. Add a warpper around
get_legacy_client to do tht one thing.

Change-Id: I086dc4a8e762d4e8e56e01cabe2386577f2ceec8
---
 README.rst                   | 31 +++++++++++++++++++++++++++++++
 os_client_config/__init__.py | 23 +++++++++++++++++++++++
 2 files changed, 54 insertions(+)

diff --git a/README.rst b/README.rst
index d8fde59..156c760 100644
--- a/README.rst
+++ b/README.rst
@@ -319,3 +319,34 @@ with - as well as a consumption argument.
   options = parser.parse_args()
 
   cloud = cloud_config.get_one_cloud(argparse=options)
+
+Constructing OpenStack Client objects
+-------------------------------------
+
+If all you want to do is get a Client object from a python-*client library,
+and you want it to do all the normal things related to clouds.yaml, `OS_`
+environment variables, a hepler function is provided.
+
+::
+
+  import argparse
+
+  from novaclient import client
+  import os_client_config
+
+  nova = os_client_config.make_client('compute', client.Client)
+
+If you want to do the same thing but also support command line parsing.
+
+::
+
+  import argparse
+
+  from novaclient import client
+  import os_client_config
+
+  nova = os_client_config.make_client(
+      'compute', client.Client, options=argparse.ArgumentParser())
+
+If you want to get fancier than that in your python, then the rest of the
+API is avaiable to you. But often times, you just want to do the one thing.
diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py
index 00e6ff5..ac585f2 100644
--- a/os_client_config/__init__.py
+++ b/os_client_config/__init__.py
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import sys
+
 from os_client_config.config import OpenStackConfig  # noqa
 
 
@@ -30,3 +32,24 @@ def simple_client(service_key, cloud=None, region_name=None):
     """
     return OpenStackConfig().get_one_cloud(
         cloud=cloud, region_name=region_name).get_session_client('compute')
+
+
+def make_client(service_key, constructor, options=None, **kwargs):
+    """Simple wrapper for getting a client instance from a client lib.
+
+    OpenStack Client Libraries all have a fairly consistent constructor
+    interface which os-client-config supports. In the simple case, there
+    is one and only one right way to construct a client object. If as a user
+    you don't want to do fancy things, just use this. It honors OS_ environment
+    variables and clouds.yaml - and takes as **kwargs anything you'd expect
+    to pass in.
+    """
+    config = OpenStackConfig()
+    if options:
+        config.register_argparse_options(options, sys.argv, service_key)
+        parsed_options = options.parse_args(sys.argv)
+    else:
+        parsed_options = None
+
+    cloud_config = config.get_one_cloud(options=parsed_options, **kwargs)
+    return cloud_config.get_legacy_client(service_key, constructor)

From b90f53bbf45c67fd2139b3f75c8f25bff0d3cfeb Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sun, 6 Dec 2015 21:49:29 -0500
Subject: [PATCH 07/39] Updated README to clarify legacy client usage

Also, update it to use code-block - which makes things look much nicer.

Change-Id: I930ab63a5d159cf4cea27b4e2c4d6fd933de04fc
---
 README.rst | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/README.rst b/README.rst
index 156c760..585dda9 100644
--- a/README.rst
+++ b/README.rst
@@ -29,7 +29,7 @@ Service specific settings, like the nova service type, are set with the
 default service type as a prefix. For instance, to set a special service_type
 for trove set
 
-::
+.. code-block:: bash
 
   export OS_DATABASE_SERVICE_TYPE=rax:database
 
@@ -56,7 +56,7 @@ Service specific settings, like the nova service type, are set with the
 default service type as a prefix. For instance, to set a special service_type
 for trove (because you're using Rackspace) set:
 
-::
+.. code-block:: yaml
 
   database_service_type: 'rax:database'
 
@@ -85,7 +85,7 @@ look in an OS specific config dir
 
 An example config file is probably helpful:
 
-::
+.. code-block:: yaml
 
   clouds:
     mordred:
@@ -155,7 +155,7 @@ the same location rules as `clouds.yaml`. It can contain anything you put
 in `clouds.yaml` and will take precedence over anything in the `clouds.yaml`
 file.
 
-::
+.. code-block:: yaml
 
   # clouds.yaml
   clouds:
@@ -209,7 +209,7 @@ that the resource should never expire.
 and presents the cache information so that your various applications that
 are connecting to OpenStack can share a cache should you desire.
 
-::
+.. code-block:: yaml
 
   cache:
     class: dogpile.cache.pylibmc
@@ -242,7 +242,7 @@ caused it to not actually function. In that case, there is a config option
 you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean
 environment variable.
 
-::
+.. code-block:: yaml
 
   client:
     force_ipv4: true
@@ -270,7 +270,7 @@ Usage
 
 The simplest and least useful thing you can do is:
 
-::
+.. code-block:: python
 
   python -m os_client_config.config
 
@@ -279,7 +279,7 @@ it from python, which is much more likely what you want to do, things like:
 
 Get a named cloud.
 
-::
+.. code-block:: python
 
   import os_client_config
 
@@ -289,7 +289,7 @@ Get a named cloud.
 
 Or, get all of the clouds.
 
-::
+.. code-block:: python
 
   import os_client_config
 
@@ -305,7 +305,7 @@ 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.
 
-::
+.. code-block:: python
 
   import argparse
   import sys
@@ -320,14 +320,14 @@ with - as well as a consumption argument.
 
   cloud = cloud_config.get_one_cloud(argparse=options)
 
-Constructing OpenStack Client objects
--------------------------------------
+Constructing Legacy Client objects
+----------------------------------
 
 If all you want to do is get a Client object from a python-*client library,
 and you want it to do all the normal things related to clouds.yaml, `OS_`
 environment variables, a hepler function is provided.
 
-::
+.. code-block:: python
 
   import argparse
 
@@ -338,7 +338,7 @@ environment variables, a hepler function is provided.
 
 If you want to do the same thing but also support command line parsing.
 
-::
+.. code-block:: python
 
   import argparse
 

From 8eced67abe20160fc3f20a7c76f01baae2dd1956 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Thu, 3 Dec 2015 13:28:00 -0600
Subject: [PATCH 08/39] Make client constructor optional

Turns out we know the mapping of service name to constsructor, so we can
try the import for the user without actually importing. Keep the
argument though, because this method should be usable by just about any
random openstack client lib. Also, because backwards compat.

Change-Id: I7e9672e3bf61b8b7b92d55903f4596382f18b515
---
 README.rst                        |  9 +++----
 os_client_config/cloud_config.py  | 41 ++++++++++++++++++++++++++++++-
 os_client_config/constructors.py  | 28 +++++++++++++++++++++
 os_client_config/constructos.json | 10 ++++++++
 4 files changed, 82 insertions(+), 6 deletions(-)
 create mode 100644 os_client_config/constructors.py
 create mode 100644 os_client_config/constructos.json

diff --git a/README.rst b/README.rst
index 156c760..7541b93 100644
--- a/README.rst
+++ b/README.rst
@@ -325,16 +325,16 @@ Constructing OpenStack Client objects
 
 If all you want to do is get a Client object from a python-*client library,
 and you want it to do all the normal things related to clouds.yaml, `OS_`
-environment variables, a hepler function is provided.
+environment variables, a hepler function is provided. The following
+will get you a fully configured `novaclient` instance.
 
 ::
 
   import argparse
 
-  from novaclient import client
   import os_client_config
 
-  nova = os_client_config.make_client('compute', client.Client)
+  nova = os_client_config.make_client('compute')
 
 If you want to do the same thing but also support command line parsing.
 
@@ -342,11 +342,10 @@ If you want to do the same thing but also support command line parsing.
 
   import argparse
 
-  from novaclient import client
   import os_client_config
 
   nova = os_client_config.make_client(
-      'compute', client.Client, options=argparse.ArgumentParser())
+      'compute', options=argparse.ArgumentParser())
 
 If you want to get fancier than that in your python, then the rest of the
 API is avaiable to you. But often times, you just want to do the one thing.
diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 18ea4c1..6b3b5d9 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -12,6 +12,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import importlib
 import warnings
 
 from keystoneauth1 import adapter
@@ -20,9 +21,44 @@ from keystoneauth1 import session
 import requestsexceptions
 
 from os_client_config import _log
+from os_client_config import constructors
 from os_client_config import exceptions
 
 
+def _get_client(service_key):
+    class_mapping = constructors.get_constructor_mapping()
+    if service_key not in class_mapping:
+        raise exceptions.OpenStackConfigException(
+            "Service {service_key} is unkown. Please pass in a client"
+            " constructor or submit a patch to os-client-config".format(
+                service_key=service_key))
+    mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1)
+    lib_name = mod_name.split('.')[0]
+    try:
+        mod = importlib.import_module(mod_name)
+    except ImportError:
+        raise exceptions.OpenStackConfigException(
+            "Client for '{service_key}' was requested, but"
+            " {mod_name} was unable to be imported. Either import"
+            " the module yourself and pass the constructor in as an argument,"
+            " or perhaps you do not have python-{lib_name} installed.".format(
+                service_key=service_key,
+                mod_name=mod_name,
+                lib_name=lib_name))
+    try:
+        ctr = getattr(mod, ctr_name)
+    except AttributeError:
+        raise exceptions.OpenStackConfigException(
+            "Client for '{service_key}' was requested, but although"
+            " {mod_name} imported fine, the constructor at {fullname}"
+            " as not found. Please check your installation, we have no"
+            " clue what is wrong with your computer.".format(
+                service_key=service_key,
+                mod_name=mod_name,
+                fullname=class_mapping[service_key]))
+    return ctr
+
+
 def _make_key(key, service_type):
     if not service_type:
         return key
@@ -217,7 +253,7 @@ class CloudConfig(object):
         return endpoint
 
     def get_legacy_client(
-            self, service_key, client_class, interface_key=None,
+            self, service_key, client_class=None, interface_key=None,
             pass_version_arg=True, **kwargs):
         """Return a legacy OpenStack client object for the given config.
 
@@ -254,6 +290,9 @@ class CloudConfig(object):
                        Client constructor, so this is in case anything
                        additional needs to be passed in.
         """
+        if not client_class:
+            client_class = _get_client(service_key)
+
         # Because of course swift is different
         if service_key == 'object-store':
             return self._get_swift_client(client_class=client_class, **kwargs)
diff --git a/os_client_config/constructors.py b/os_client_config/constructors.py
new file mode 100644
index 0000000..e88ac92
--- /dev/null
+++ b/os_client_config/constructors.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import os
+
+_json_path = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), 'constructors.json')
+_class_mapping = None
+
+
+def get_constructor_mapping():
+    global _class_mapping
+    if not _class_mapping:
+        with open(_json_path, 'r') as json_file:
+            _class_mapping = json.load(json_file)
+    return _class_mapping
diff --git a/os_client_config/constructos.json b/os_client_config/constructos.json
new file mode 100644
index 0000000..d9ebf2c
--- /dev/null
+++ b/os_client_config/constructos.json
@@ -0,0 +1,10 @@
+{
+    "compute": "novaclient.client.Client",
+    "database": "troveclient.client.Client",
+    "identity": "keystoneclient.client.Client",
+    "image": "glanceclient.Client",
+    "network": "neutronclient.neutron.client.Client",
+    "object-store": "swiftclient.client.Connection",
+    "orchestration": "heatclient.client.Client",
+    "volume": "cinderclient.client.Client"
+}

From 1221ea7fca67c22c455b4aeae1e09c9ad7e928a7 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Fri, 4 Dec 2015 13:19:19 -0500
Subject: [PATCH 09/39] Fix a README typo - hepler is not actually a thing

Change-Id: Ie8c267e75171b88bd3a1a3a684e85869e75843d7
---
 README.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.rst b/README.rst
index 7541b93..4edff9e 100644
--- a/README.rst
+++ b/README.rst
@@ -325,7 +325,7 @@ Constructing OpenStack Client objects
 
 If all you want to do is get a Client object from a python-*client library,
 and you want it to do all the normal things related to clouds.yaml, `OS_`
-environment variables, a hepler function is provided. The following
+environment variables, a helper function is provided. The following
 will get you a fully configured `novaclient` instance.
 
 ::

From d0c70cc96279d2bf24f30a501b9bf572e40f8e7a Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sun, 22 Nov 2015 10:55:46 -0500
Subject: [PATCH 10/39] Add support for generalized per-region settings

Internap creates a public and a private network for each customer for
each region on region activation. This means there is a per-region
external network that the user may want to specify. Also, conoha has
per-region auth-urls. Per-region config is still
overridden by argparse or kwargs values.

Change-Id: Ie2f3d2ca3ccbe7e3dd674983136b42c323544997
---
 README.rst                            | 32 ++++++++++
 os_client_config/config.py            | 90 ++++++++++++++++++---------
 os_client_config/tests/base.py        | 17 ++++-
 os_client_config/tests/test_config.py | 50 ++++++++++++---
 4 files changed, 146 insertions(+), 43 deletions(-)

diff --git a/README.rst b/README.rst
index 9850c05..ced3b18 100644
--- a/README.rst
+++ b/README.rst
@@ -265,6 +265,38 @@ environment variable.
 The above snippet will tell client programs to prefer returning an IPv4
 address.
 
+Per-region settings
+-------------------
+
+Sometimes you have a cloud provider that has config that is common to the
+cloud, but also with some things you might want to express on a per-region
+basis. For instance, Internap provides a public and private network specific
+to the user in each region, and putting the values of those networks into
+config can make consuming programs more efficient.
+
+To support this, the region list can actually be a list of dicts, and any
+setting that can be set at the cloud level can be overridden for that
+region.
+
+::
+
+  clouds:
+    internap:
+      profile: internap
+      auth:
+        password: XXXXXXXXXXXXXXXXX
+        username: api-55f9a00fb2619
+        project_name: inap-17037
+      regions:
+      - name: ams01
+        values:
+          external_network: inap-17037-WAN1654
+          internal_network: inap-17037-LAN4820
+      - name: nyj01
+        values:
+          external_network: inap-17037-WAN7752
+          internal_network: inap-17037-LAN6745
+
 Usage
 -----
 
diff --git a/os_client_config/config.py b/os_client_config/config.py
index 48aa0e2..70989bf 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -15,6 +15,7 @@
 
 # alias because we already had an option named argparse
 import argparse as argparse_mod
+import copy
 import json
 import os
 import warnings
@@ -121,8 +122,9 @@ def _merge_clouds(old_dict, new_dict):
     return ret
 
 
-def _auth_update(old_dict, new_dict):
+def _auth_update(old_dict, new_dict_source):
     """Like dict.update, except handling the nested dict called auth."""
+    new_dict = copy.deepcopy(new_dict_source)
     for (k, v) in new_dict.items():
         if k == 'auth':
             if k in old_dict:
@@ -302,17 +304,29 @@ class OpenStackConfig(object):
         return self._cache_class
 
     def get_cache_arguments(self):
-        return self._cache_arguments.copy()
+        return copy.deepcopy(self._cache_arguments)
 
     def get_cache_expiration(self):
-        return self._cache_expiration.copy()
+        return copy.deepcopy(self._cache_expiration)
+
+    def _expand_region_name(self, region_name):
+        return {'name': region_name, 'values': {}}
+
+    def _expand_regions(self, regions):
+        ret = []
+        for region in regions:
+            if isinstance(region, dict):
+                ret.append(copy.deepcopy(region))
+            else:
+                ret.append(self._expand_region_name(region))
+        return ret
 
     def _get_regions(self, cloud):
         if cloud not in self.cloud_config['clouds']:
-            return ['']
+            return [self._expand_region_name('')]
         config = self._normalize_keys(self.cloud_config['clouds'][cloud])
         if 'regions' in config:
-            return config['regions']
+            return self._expand_regions(config['regions'])
         elif 'region_name' in config:
             regions = config['region_name'].split(',')
             if len(regions) > 1:
@@ -320,22 +334,39 @@ class OpenStackConfig(object):
                     "Comma separated lists in region_name are deprecated."
                     " Please use a yaml list in the regions"
                     " parameter in {0} instead.".format(self.config_filename))
-            return regions
+            return self._expand_regions(regions)
         else:
             # crappit. we don't have a region defined.
             new_cloud = dict()
             our_cloud = self.cloud_config['clouds'].get(cloud, dict())
             self._expand_vendor_profile(cloud, new_cloud, our_cloud)
             if 'regions' in new_cloud and new_cloud['regions']:
-                return new_cloud['regions']
+                return self._expand_regions(new_cloud['regions'])
             elif 'region_name' in new_cloud and new_cloud['region_name']:
-                return [new_cloud['region_name']]
+                return [self._expand_region_name(new_cloud['region_name'])]
             else:
                 # Wow. We really tried
-                return ['']
+                return [self._expand_region_name('')]
 
-    def _get_region(self, cloud=None):
-        return self._get_regions(cloud)[0]
+    def _get_region(self, cloud=None, region_name=''):
+        if not cloud:
+            return self._expand_region_name(region_name)
+
+        regions = self._get_regions(cloud)
+        if not region_name:
+            return regions[0]
+
+        for region in regions:
+            if region['name'] == region_name:
+                return region
+
+        raise exceptions.OpenStackConfigException(
+            'Region {region_name} is not a valid region name for cloud'
+            ' {cloud}. Valid choices are {region_list}. Please note that'
+            ' region names are case sensitive.'.format(
+                region_name=region_name,
+                region_list=','.join([r['name'] for r in regions]),
+                cloud=cloud))
 
     def get_cloud_names(self):
         return self.cloud_config['clouds'].keys()
@@ -585,7 +616,9 @@ class OpenStackConfig(object):
 
         for cloud in self.get_cloud_names():
             for region in self._get_regions(cloud):
-                clouds.append(self.get_one_cloud(cloud, region_name=region))
+                if region:
+                    clouds.append(self.get_one_cloud(
+                        cloud, region_name=region['name']))
         return clouds
 
     def _fix_args(self, args, argparse=None):
@@ -764,30 +797,27 @@ class OpenStackConfig(object):
             else:
                 cloud = self.default_cloud
 
-        if 'region_name' not in args or args['region_name'] is None:
-            args['region_name'] = self._get_region(cloud)
-
         config = self._get_base_cloud_config(cloud)
 
+        # Get region specific settings
+        if 'region_name' not in args:
+            args['region_name'] = ''
+        region = self._get_region(cloud=cloud, region_name=args['region_name'])
+        args['region_name'] = region['name']
+        region_args = copy.deepcopy(region['values'])
+
         # Regions is a list that we can use to create a list of cloud/region
         # objects. It does not belong in the single-cloud dict
-        regions = config.pop('regions', None)
-        if regions and args['region_name'] not in regions:
-            raise exceptions.OpenStackConfigException(
-                'Region {region_name} is not a valid region name for cloud'
-                ' {cloud}. Valid choices are {region_list}. Please note that'
-                ' region names are case sensitive.'.format(
-                    region_name=args['region_name'],
-                    region_list=','.join(regions),
-                    cloud=cloud))
+        config.pop('regions', None)
 
         # Can't just do update, because None values take over
-        for (key, val) in iter(args.items()):
-            if val is not None:
-                if key == 'auth' and config[key] is not None:
-                    config[key] = _auth_update(config[key], val)
-                else:
-                    config[key] = val
+        for arg_list in region_args, args:
+            for (key, val) in iter(arg_list.items()):
+                if val is not None:
+                    if key == 'auth' and config[key] is not None:
+                        config[key] = _auth_update(config[key], val)
+                    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
diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py
index 33a868d..6d9e093 100644
--- a/os_client_config/tests/base.py
+++ b/os_client_config/tests/base.py
@@ -16,6 +16,7 @@
 # under the License.
 
 
+import copy
 import os
 import tempfile
 
@@ -96,8 +97,18 @@ USER_CONF = {
                 'auth_url': 'http://example.com/v2',
             },
             'regions': [
-                'region1',
-                'region2',
+                {
+                    'name': 'region1',
+                    'values': {
+                        'external_network': 'region1-network',
+                    }
+                },
+                {
+                    'name': 'region2',
+                    'values': {
+                        'external_network': 'my-network',
+                    }
+                }
             ],
         },
         '_test_cloud_hyphenated': {
@@ -139,7 +150,7 @@ class TestCase(base.BaseTestCase):
         super(TestCase, self).setUp()
 
         self.useFixture(fixtures.NestedTempfile())
-        conf = dict(USER_CONF)
+        conf = copy.deepcopy(USER_CONF)
         tdir = self.useFixture(fixtures.TempDir())
         conf['cache']['path'] = tdir.path
         self.cloud_yaml = _write_yaml(conf)
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index c9318fc..a6a35ad 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -226,6 +226,8 @@ class TestConfig(base.TestCase):
                                              new_config)
         with open(self.cloud_yaml) as fh:
             written_config = yaml.safe_load(fh)
+            # We write a cache config for testing
+            written_config['cache'].pop('path', None)
             self.assertEqual(written_config, resulting_config)
 
 
@@ -239,18 +241,26 @@ class TestConfigArgparse(base.TestCase):
             username='user',
             password='password',
             project_name='project',
-            region_name='other-test-region',
+            region_name='region2',
             snack_type='cookie',
         )
         self.options = argparse.Namespace(**self.args)
 
+    def test_get_one_cloud_bad_region_argparse(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        self.assertRaises(
+            exceptions.OpenStackConfigException, c.get_one_cloud,
+            cloud='_test-cloud_', argparse=self.options)
+
     def test_get_one_cloud_argparse(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])
 
-        cc = c.get_one_cloud(cloud='_test-cloud_', argparse=self.options)
-        self._assert_cloud_details(cc)
-        self.assertEqual(cc.region_name, 'other-test-region')
+        cc = c.get_one_cloud(
+            cloud='_test_cloud_regions', argparse=self.options)
+        self.assertEqual(cc.region_name, 'region2')
         self.assertEqual(cc.snack_type, 'cookie')
 
     def test_get_one_cloud_just_argparse(self):
@@ -259,7 +269,7 @@ class TestConfigArgparse(base.TestCase):
 
         cc = c.get_one_cloud(argparse=self.options)
         self.assertIsNone(cc.cloud)
-        self.assertEqual(cc.region_name, 'other-test-region')
+        self.assertEqual(cc.region_name, 'region2')
         self.assertEqual(cc.snack_type, 'cookie')
 
     def test_get_one_cloud_just_kwargs(self):
@@ -268,7 +278,7 @@ class TestConfigArgparse(base.TestCase):
 
         cc = c.get_one_cloud(**self.args)
         self.assertIsNone(cc.cloud)
-        self.assertEqual(cc.region_name, 'other-test-region')
+        self.assertEqual(cc.region_name, 'region2')
         self.assertEqual(cc.snack_type, 'cookie')
 
     def test_get_one_cloud_dash_kwargs(self):
@@ -318,10 +328,10 @@ class TestConfigArgparse(base.TestCase):
     def test_get_one_cloud_bad_region_no_regions(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])
-
-        cc = c.get_one_cloud(cloud='_test-cloud_', region_name='bad_region')
-        self._assert_cloud_details(cc)
-        self.assertEqual(cc.region_name, 'bad_region')
+        self.assertRaises(
+            exceptions.OpenStackConfigException,
+            c.get_one_cloud,
+            cloud='_test-cloud_', region_name='bad_region')
 
     def test_get_one_cloud_no_argparse_region2(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
@@ -333,6 +343,26 @@ class TestConfigArgparse(base.TestCase):
         self.assertEqual(cc.region_name, 'region2')
         self.assertIsNone(cc.snack_type)
 
+    def test_get_one_cloud_network(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        cc = c.get_one_cloud(
+            cloud='_test_cloud_regions', region_name='region1', argparse=None)
+        self._assert_cloud_details(cc)
+        self.assertEqual(cc.region_name, 'region1')
+        self.assertEqual('region1-network', cc.config['external_network'])
+
+    def test_get_one_cloud_per_region_network(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        cc = c.get_one_cloud(
+            cloud='_test_cloud_regions', region_name='region2', argparse=None)
+        self._assert_cloud_details(cc)
+        self.assertEqual(cc.region_name, 'region2')
+        self.assertEqual('my-network', cc.config['external_network'])
+
     def test_fix_env_args(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])

From f4237a809cccbbffce2233b1f283b36a9ebb75c1 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 9 Dec 2015 15:42:20 -0500
Subject: [PATCH 11/39] Add ceilometer constructor to known constructors

In porting ospurge to use get_legacy_client, it became clear that
the ceilometer client constructor was missing. Add it.

Change-Id: I1102105b78574378c4f11064e21245b08513247b
---
 os_client_config/{constructos.json => constructors.json} | 1 +
 os_client_config/defaults.json                           | 1 +
 2 files changed, 2 insertions(+)
 rename os_client_config/{constructos.json => constructors.json} (88%)

diff --git a/os_client_config/constructos.json b/os_client_config/constructors.json
similarity index 88%
rename from os_client_config/constructos.json
rename to os_client_config/constructors.json
index d9ebf2c..be44339 100644
--- a/os_client_config/constructos.json
+++ b/os_client_config/constructors.json
@@ -3,6 +3,7 @@
     "database": "troveclient.client.Client",
     "identity": "keystoneclient.client.Client",
     "image": "glanceclient.Client",
+    "metering": "ceilometerclient.client.Client",
     "network": "neutronclient.neutron.client.Client",
     "object-store": "swiftclient.client.Connection",
     "orchestration": "heatclient.client.Client",
diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json
index eb8162e..6735b55 100644
--- a/os_client_config/defaults.json
+++ b/os_client_config/defaults.json
@@ -12,6 +12,7 @@
   "image_api_use_tasks": false,
   "image_api_version": "2",
   "image_format": "qcow2",
+  "metering_api_version": "2",
   "network_api_version": "2",
   "object_store_api_version": "1",
   "orchestration_api_version": "1",

From 837ca712288ceffea5b54ceaeb349d6577f38360 Mon Sep 17 00:00:00 2001
From: David Shrewsbury <shrewsbury.dave@gmail.com>
Date: Tue, 17 Nov 2015 15:17:55 -0500
Subject: [PATCH 12/39] Allow arbitrary client-specific options

There are occasionally some client-specific things that would be handy
to be able to configure about behaviors. For instance, the only config
file that ansible's openstack inventory has is clouds.yaml. Rather than
teaching os-client-config about such things, allow a pass-through config
section. Apply key normalization to _'s like other configs, and merge
the clouds and secure files so that the sections behave like other OCC
config sections.

Change-Id: If307e95006abf6e1efbbd77cfc99e5fdfed6c80a
---
 os_client_config/config.py            | 13 +++++++++++++
 os_client_config/tests/base.py        |  4 ++++
 os_client_config/tests/test_config.py | 21 +++++++++++++++++++++
 3 files changed, 38 insertions(+)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 70989bf..ab3a003 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -253,6 +253,19 @@ class OpenStackConfig(object):
         # Flag location to hold the peeked value of an argparse timeout value
         self._argv_timeout = False
 
+    def get_extra_config(self, key, defaults=None):
+        """Fetch an arbitrary extra chunk of config, laying in defaults.
+
+        :param string key: name of the config section to fetch
+        :param dict defaults: (optional) default values to merge under the
+                                         found config
+        """
+        if not defaults:
+            defaults = {}
+        return _merge_clouds(
+            self._normalize_keys(defaults),
+            self._normalize_keys(self.cloud_config.get(key, {})))
+
     def _load_config_file(self):
         return self._load_yaml_json_file(self._config_files)
 
diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py
index 6d9e093..fdc50cd 100644
--- a/os_client_config/tests/base.py
+++ b/os_client_config/tests/base.py
@@ -121,6 +121,10 @@ USER_CONF = {
             'region_name': 'test-region',
         }
     },
+    'ansible': {
+        'expand-hostvars': False,
+        'use_hostnames': True,
+    },
 }
 SECURE_CONF = {
     'clouds': {
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index a6a35ad..98aaf79 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -372,6 +372,27 @@ class TestConfigArgparse(base.TestCase):
 
         self.assertDictEqual({'compute_api_version': 1}, fixed_args)
 
+    def test_extra_config(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        defaults = {'use_hostnames': False, 'other-value': 'something'}
+        ansible_options = c.get_extra_config('ansible', defaults)
+
+        # This should show that the default for use_hostnames above is
+        # overridden by the value in the config file defined in base.py
+        # It should also show that other-value key is normalized and passed
+        # through even though there is no corresponding value in the config
+        # file, and that expand-hostvars key is normalized and the value
+        # from the config comes through even though there is no default.
+        self.assertDictEqual(
+            {
+                'expand_hostvars': False,
+                'use_hostnames': True,
+                'other_value': 'something',
+            },
+            ansible_options)
+
     def test_register_argparse_cloud(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])

From 88b7e643b9637864252d9c6b715da0dced606352 Mon Sep 17 00:00:00 2001
From: Shuquan Huang <huang.shuquan@99cloud.net>
Date: Thu, 17 Dec 2015 13:58:10 +0800
Subject: [PATCH 13/39] Replace assertEqual(None, *) with assertIsNone in tests

Replace assertEqual(None, *) with assertIsNone in tests to have
more clear messages in case of failure.

Change-Id: Ia1af9f64f4f0a66c1429d81313b2c27a7c67cdd7
Closes-bug: #1280522
---
 os_client_config/tests/test_cloud_config.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py
index 9e683d1..322973a 100644
--- a/os_client_config/tests/test_cloud_config.py
+++ b/os_client_config/tests/test_cloud_config.py
@@ -47,7 +47,7 @@ class TestCloudConfig(base.TestCase):
         self.assertEqual(1, cc.a)
 
         # Look up prefixed attribute, fail - returns None
-        self.assertEqual(None, cc.os_b)
+        self.assertIsNone(cc.os_b)
 
         # Look up straight value, then prefixed value
         self.assertEqual(3, cc.c)
@@ -139,7 +139,7 @@ class TestCloudConfig(base.TestCase):
         self.assertEqual('region-al', cc.get_region_name())
         self.assertEqual('region-al', cc.get_region_name('image'))
         self.assertEqual('region-bl', cc.get_region_name('compute'))
-        self.assertEqual(None, cc.get_api_version('image'))
+        self.assertIsNone(cc.get_api_version('image'))
         self.assertEqual('2', cc.get_api_version('compute'))
         self.assertEqual('mage', cc.get_service_type('image'))
         self.assertEqual('compute', cc.get_service_type('compute'))
@@ -149,7 +149,7 @@ class TestCloudConfig(base.TestCase):
                          cc.get_endpoint('compute'))
         self.assertEqual(None,
                          cc.get_endpoint('image'))
-        self.assertEqual(None, cc.get_service_name('compute'))
+        self.assertIsNone(cc.get_service_name('compute'))
         self.assertEqual('locks', cc.get_service_name('identity'))
 
     def test_volume_override(self):

From 22d740b7007e1182c99370cb2629322384b17a14 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sat, 12 Dec 2015 10:53:53 -0500
Subject: [PATCH 14/39] Add backwards compat mapping for auth-token

novaclient accepted an auth-token argument, which also triggered a token
not password based workflow. That's fine - let's map that to token, and
if we find it, change auth_type's default from password to token.

Change-Id: Ie9acece5cb3c68560ae975bfb0fb2393381b6fba
---
 os_client_config/config.py            | 15 ++++++++++++++
 os_client_config/tests/test_config.py | 30 +++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index ab3a003..48bcb0f 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -467,6 +467,7 @@ class OpenStackConfig(object):
             'project_domain_id': ('project_domain_id', 'project-domain-id'),
             'project_domain_name': (
                 'project_domain_name', 'project-domain-name'),
+            'token': ('auth-token', 'auth_token', 'token'),
         }
         for target_key, possible_values in mappings.items():
             target = None
@@ -535,6 +536,13 @@ class OpenStackConfig(object):
         # for from the user passing it explicitly. We'll stash it for later
         local_parser.add_argument('--timeout', metavar='<timeout>')
 
+        # We need for get_one_cloud to be able to peek at whether a token
+        # was passed so that we can swap the default from password to
+        # token if it was. And we need to also peek for --os-auth-token
+        # for novaclient backwards compat
+        local_parser.add_argument('--os-token')
+        local_parser.add_argument('--os-auth-token')
+
         # 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
@@ -832,6 +840,13 @@ class OpenStackConfig(object):
                     else:
                         config[key] = val
 
+        # Infer token plugin if a token was given
+        if (('auth' in config and 'token' in config['auth']) or
+                ('auth_token' in config and config['auth_token']) or
+                ('token' in config and config['token'])):
+            config['auth_type'] = 'token'
+            config.setdefault('token', config.pop('auth_token', None))
+
         # 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)
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index 98aaf79..bb45693 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -243,7 +243,9 @@ class TestConfigArgparse(base.TestCase):
             project_name='project',
             region_name='region2',
             snack_type='cookie',
+            os_auth_token='no-good-things',
         )
+
         self.options = argparse.Namespace(**self.args)
 
     def test_get_one_cloud_bad_region_argparse(self):
@@ -401,6 +403,34 @@ class TestConfigArgparse(base.TestCase):
         opts, _remain = parser.parse_known_args(['--os-cloud', 'foo'])
         self.assertEqual(opts.os_cloud, 'foo')
 
+    def test_argparse_default_no_token(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        parser = argparse.ArgumentParser()
+        c.register_argparse_arguments(parser, [])
+        # novaclient will add this
+        parser.add_argument('--os-auth-token')
+        opts, _remain = parser.parse_known_args()
+        cc = c.get_one_cloud(
+            cloud='_test_cloud_regions', argparse=opts)
+        self.assertEqual(cc.config['auth_type'], 'password')
+        self.assertNotIn('token', cc.config['auth'])
+
+    def test_argparse_token(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        parser = argparse.ArgumentParser()
+        c.register_argparse_arguments(parser, [])
+        # novaclient will add this
+        parser.add_argument('--os-auth-token')
+        opts, _remain = parser.parse_known_args(
+            ['--os-auth-token', 'very-bad-things'])
+        cc = c.get_one_cloud(argparse=opts)
+        self.assertEqual(cc.config['auth_type'], 'token')
+        self.assertEqual(cc.config['auth']['token'], 'very-bad-things')
+
     def test_register_argparse_bad_plugin(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])

From 3a34378f712abee2d525973815a99188c598d726 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sat, 12 Dec 2015 13:03:07 -0500
Subject: [PATCH 15/39] Support backwards compat for _ args

Instead of putting tons of hidden options to allow for variations of
argparse options with _ in them, just manipulate the argv when it's
passed in to translate to - instead. (why the heck does argparse not
already do this?)

Change-Id: I5f0bd9d9a333781ad13d531b3667fff5fdac9eac
---
 os_client_config/config.py            | 32 +++++++++++++++++++++++++++
 os_client_config/tests/test_config.py | 31 ++++++++++++++++++++++++++
 2 files changed, 63 insertions(+)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 48bcb0f..077c109 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -15,6 +15,7 @@
 
 # alias because we already had an option named argparse
 import argparse as argparse_mod
+import collections
 import copy
 import json
 import os
@@ -136,6 +137,34 @@ def _auth_update(old_dict, new_dict_source):
     return old_dict
 
 
+def _fix_argv(argv):
+    # Transform any _ characters in arg names to - so that we don't
+    # have to throw billions of compat argparse arguments around all
+    # over the place.
+    processed = collections.defaultdict(list)
+    for index in range(0, len(argv)):
+        if argv[index].startswith('--'):
+            split_args = argv[index].split('=')
+            orig = split_args[0]
+            new = orig.replace('_', '-')
+            if orig != new:
+                split_args[0] = new
+                argv[index] = "=".join(split_args)
+            # Save both for later so we can throw an error about dupes
+            processed[new].append(orig)
+    overlap = []
+    for new, old in processed.items():
+        if len(old) > 1:
+            overlap.extend(old)
+    if overlap:
+        raise exceptions.OpenStackConfigException(
+            "The following options were given: '{options}' which contain"
+            " duplicates except that one has _ and one has -. There is"
+            " no sane way for us to know what you're doing. Remove the"
+            " duplicate option and try again".format(
+                options=','.join(overlap)))
+
+
 class OpenStackConfig(object):
 
     def __init__(self, config_files=None, vendor_files=None,
@@ -521,6 +550,9 @@ class OpenStackConfig(object):
                                                     is requested
         """
 
+        # Fix argv in place - mapping any keys with embedded _ in them to -
+        _fix_argv(argv)
+
         local_parser = argparse_mod.ArgumentParser(add_help=False)
 
         for p in (parser, local_parser):
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index bb45693..30ed731 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -431,6 +431,37 @@ class TestConfigArgparse(base.TestCase):
         self.assertEqual(cc.config['auth_type'], 'token')
         self.assertEqual(cc.config['auth']['token'], 'very-bad-things')
 
+    def test_argparse_underscores(self):
+        c = config.OpenStackConfig(config_files=[self.no_yaml],
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
+        parser = argparse.ArgumentParser()
+        parser.add_argument('--os_username')
+        argv = [
+            '--os_username', 'user', '--os_password', 'pass',
+            '--os-auth-url', 'auth-url', '--os-project-name', 'project']
+        c.register_argparse_arguments(parser, argv=argv)
+        opts, _remain = parser.parse_known_args(argv)
+        cc = c.get_one_cloud(argparse=opts)
+        self.assertEqual(cc.config['auth']['username'], 'user')
+        self.assertEqual(cc.config['auth']['password'], 'pass')
+        self.assertEqual(cc.config['auth']['auth_url'], 'auth-url')
+
+    def test_argparse_underscores_duplicate(self):
+        c = config.OpenStackConfig(config_files=[self.no_yaml],
+                                   vendor_files=[self.no_yaml],
+                                   secure_files=[self.no_yaml])
+        parser = argparse.ArgumentParser()
+        parser.add_argument('--os_username')
+        argv = [
+            '--os_username', 'user', '--os_password', 'pass',
+            '--os-username', 'user1', '--os-password', 'pass1',
+            '--os-auth-url', 'auth-url', '--os-project-name', 'project']
+        self.assertRaises(
+            exceptions.OpenStackConfigException,
+            c.register_argparse_arguments,
+            parser=parser, argv=argv)
+
     def test_register_argparse_bad_plugin(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])

From 16166c03c27fe73896a1717ad9a145a466bc0afd Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sat, 12 Dec 2015 13:03:41 -0500
Subject: [PATCH 16/39] Pass endpoint override to constructors

Also, the variable name from keystoneauth is "*-endpoint-override" ...
so we need to respond to that. Respond to the old -endpoint for compat
reasons. Then let's actually pass in the value.

Change-Id: I6f413b02e0d2b167a4ee30494b2c91c67124b219
---
 os_client_config/cloud_config.py            |  6 ++++--
 os_client_config/tests/test_cloud_config.py | 22 ++++++++++++++++++---
 2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 6b3b5d9..2f3c94a 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -164,8 +164,9 @@ class CloudConfig(object):
         return self.config.get(key, None)
 
     def get_endpoint(self, service_type):
-        key = _make_key('endpoint', service_type)
-        return self.config.get(key, None)
+        key = _make_key('endpoint_override', service_type)
+        old_key = _make_key('endpoint', service_type)
+        return self.config.get(key, self.config.get(old_key, None))
 
     @property
     def prefer_ipv6(self):
@@ -310,6 +311,7 @@ class CloudConfig(object):
             session=self.get_session(),
             service_name=self.get_service_name(service_key),
             service_type=self.get_service_type(service_key),
+            endpoint_override=self.get_endpoint(service_key),
             region_name=self.region)
 
         if service_key == 'image':
diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py
index 322973a..3412254 100644
--- a/os_client_config/tests/test_cloud_config.py
+++ b/os_client_config/tests/test_cloud_config.py
@@ -25,8 +25,9 @@ from os_client_config.tests import base
 fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4}
 fake_services_dict = {
     'compute_api_version': '2',
-    'compute_endpoint': 'http://compute.example.com',
+    'compute_endpoint_override': 'http://compute.example.com',
     'compute_region_name': 'region-bl',
+    'telemetry_endpoint': 'http://telemetry.example.com',
     'interface': 'public',
     'image_service_type': 'mage',
     'identity_interface': 'admin',
@@ -189,14 +190,24 @@ class TestCloudConfig(base.TestCase):
             verify=True, cert=None, timeout=9)
 
     @mock.patch.object(ksa_session, 'Session')
-    def test_override_session_endpoint(self, mock_session):
+    def test_override_session_endpoint_override(self, mock_session):
         config_dict = defaults.get_defaults()
         config_dict.update(fake_services_dict)
         cc = cloud_config.CloudConfig(
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         self.assertEqual(
             cc.get_session_endpoint('compute'),
-            fake_services_dict['compute_endpoint'])
+            fake_services_dict['compute_endpoint_override'])
+
+    @mock.patch.object(ksa_session, 'Session')
+    def test_override_session_endpoint(self, mock_session):
+        config_dict = defaults.get_defaults()
+        config_dict.update(fake_services_dict)
+        cc = cloud_config.CloudConfig(
+            "test1", "region-al", config_dict, auth_plugin=mock.Mock())
+        self.assertEqual(
+            cc.get_session_endpoint('telemetry'),
+            fake_services_dict['telemetry_endpoint'])
 
     @mock.patch.object(cloud_config.CloudConfig, 'get_session')
     def test_session_endpoint_identity(self, mock_get_session):
@@ -297,6 +308,7 @@ class TestCloudConfig(base.TestCase):
             '2',
             service_name=None,
             endpoint='http://example.com',
+            endpoint_override=None,
             region_name='region-al',
             interface='public',
             session=mock.ANY,
@@ -316,6 +328,7 @@ class TestCloudConfig(base.TestCase):
         mock_client.assert_called_with(
             '2.0',
             endpoint_type='public',
+            endpoint_override=None,
             region_name='region-al',
             service_type='network',
             session=mock.ANY,
@@ -333,6 +346,7 @@ class TestCloudConfig(base.TestCase):
         mock_client.assert_called_with(
             '2',
             endpoint_type='public',
+            endpoint_override='http://compute.example.com',
             region_name='region-al',
             service_type='compute',
             session=mock.ANY,
@@ -351,6 +365,7 @@ class TestCloudConfig(base.TestCase):
             '2.0',
             endpoint='http://example.com/v2',
             endpoint_type='admin',
+            endpoint_override=None,
             region_name='region-al',
             service_type='identity',
             session=mock.ANY,
@@ -370,6 +385,7 @@ class TestCloudConfig(base.TestCase):
             '3',
             endpoint='http://example.com',
             endpoint_type='admin',
+            endpoint_override=None,
             region_name='region-al',
             service_type='identity',
             session=mock.ANY,

From 0a25cb5c5059fc8f850e27a984613515ae76ee11 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Sat, 12 Dec 2015 17:26:09 -0500
Subject: [PATCH 17/39] Allow passing in explicit version for legacy_client

Nova (and indeed other clients with microversions, need a user to be
able to request an explicit version.

Change-Id: I5f67b7fc007b7d6123f621c5943345f88db1f84b
---
 os_client_config/cloud_config.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 2f3c94a..0233eff 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -255,7 +255,7 @@ class CloudConfig(object):
 
     def get_legacy_client(
             self, service_key, client_class=None, interface_key=None,
-            pass_version_arg=True, **kwargs):
+            pass_version_arg=True, version=None, **kwargs):
         """Return a legacy OpenStack client object for the given config.
 
         Most of the OpenStack python-*client libraries have the same
@@ -287,6 +287,8 @@ class CloudConfig(object):
                                  already understand that this is the
                                  case for network, so it can be omitted in
                                  that case.
+        :param version: (optional) Version string to override the configured
+                                   version string.
         :param kwargs: (optional) keyword args are passed through to the
                        Client constructor, so this is in case anything
                        additional needs to be passed in.
@@ -320,13 +322,14 @@ class CloudConfig(object):
             # would need to do if they were requesting 'image' - then
             # they necessarily have glanceclient installed
             from glanceclient.common import utils as glance_utils
-            endpoint, version = glance_utils.strip_version(endpoint)
+            endpoint, _ = glance_utils.strip_version(endpoint)
             constructor_kwargs['endpoint'] = endpoint
         constructor_kwargs.update(kwargs)
         constructor_kwargs[interface_key] = interface
         constructor_args = []
         if pass_version_arg:
-            version = self.get_api_version(service_key)
+            if not version:
+                version = self.get_api_version(service_key)
             # Temporary workaround while we wait for python-openstackclient
             # to be able to handle 2.0 which is what neutronclient expects
             if service_key == 'network' and version == '2':

From 939862e55e42c5fafee9c2fec42b5f5fde8fc205 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Mon, 21 Dec 2015 11:35:56 -0600
Subject: [PATCH 18/39] Fix glance endpoints with endpoint_override

Now that we properly pass endpoint_override all the time, we broke
glance. The reason for this is that we calculate the glance url via
glance url stripping in all cases, so the case where we did not have
a configured endpoint override was passing the wrong information to the
constructor, causing double version addition.

Change-Id: I5699b0581d0cb68fed68800c29c8a847e2606ec9
---
 os_client_config/cloud_config.py            | 15 +++-
 os_client_config/tests/test_cloud_config.py | 92 ++++++++++++++++++++-
 2 files changed, 101 insertions(+), 6 deletions(-)

diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 0233eff..f73da04 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -302,6 +302,7 @@ class CloudConfig(object):
         interface = self.get_interface(service_key)
         # trigger exception on lack of service
         endpoint = self.get_session_endpoint(service_key)
+        endpoint_override = self.get_endpoint(service_key)
 
         if not interface_key:
             if service_key == 'image':
@@ -313,7 +314,7 @@ class CloudConfig(object):
             session=self.get_session(),
             service_name=self.get_service_name(service_key),
             service_type=self.get_service_type(service_key),
-            endpoint_override=self.get_endpoint(service_key),
+            endpoint_override=endpoint_override,
             region_name=self.region)
 
         if service_key == 'image':
@@ -322,8 +323,16 @@ class CloudConfig(object):
             # would need to do if they were requesting 'image' - then
             # they necessarily have glanceclient installed
             from glanceclient.common import utils as glance_utils
-            endpoint, _ = glance_utils.strip_version(endpoint)
-            constructor_kwargs['endpoint'] = endpoint
+            endpoint, detected_version = glance_utils.strip_version(endpoint)
+            # If the user has passed in a version, that's explicit, use it
+            if not version:
+                version = detected_version
+            # If the user has passed in or configured an override, use it.
+            # Otherwise, ALWAYS pass in an endpoint_override becuase
+            # we've already done version stripping, so we don't want version
+            # reconstruction to happen twice
+            if not endpoint_override:
+                constructor_kwargs['endpoint_override'] = endpoint
         constructor_kwargs.update(kwargs)
         constructor_kwargs[interface_key] = interface
         constructor_args = []
diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py
index 3412254..01581b1 100644
--- a/os_client_config/tests/test_cloud_config.py
+++ b/os_client_config/tests/test_cloud_config.py
@@ -305,10 +305,96 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client)
         mock_client.assert_called_with(
-            '2',
+            2.0,
             service_name=None,
-            endpoint='http://example.com',
-            endpoint_override=None,
+            endpoint_override='http://example.com',
+            region_name='region-al',
+            interface='public',
+            session=mock.ANY,
+            # Not a typo - the config dict above overrides this
+            service_type='mage'
+        )
+
+    @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
+    def test_legacy_client_image_override(self, mock_get_session_endpoint):
+        mock_client = mock.Mock()
+        mock_get_session_endpoint.return_value = 'http://example.com/v2'
+        config_dict = defaults.get_defaults()
+        config_dict.update(fake_services_dict)
+        config_dict['image_endpoint_override'] = 'http://example.com/override'
+        cc = cloud_config.CloudConfig(
+            "test1", "region-al", config_dict, auth_plugin=mock.Mock())
+        cc.get_legacy_client('image', mock_client)
+        mock_client.assert_called_with(
+            2.0,
+            service_name=None,
+            endpoint_override='http://example.com/override',
+            region_name='region-al',
+            interface='public',
+            session=mock.ANY,
+            # Not a typo - the config dict above overrides this
+            service_type='mage'
+        )
+
+    @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
+    def test_legacy_client_image_versioned(self, mock_get_session_endpoint):
+        mock_client = mock.Mock()
+        mock_get_session_endpoint.return_value = 'http://example.com/v2'
+        config_dict = defaults.get_defaults()
+        config_dict.update(fake_services_dict)
+        # v2 endpoint was passed, 1 requested in config, endpoint wins
+        config_dict['image_api_version'] = '1'
+        cc = cloud_config.CloudConfig(
+            "test1", "region-al", config_dict, auth_plugin=mock.Mock())
+        cc.get_legacy_client('image', mock_client)
+        mock_client.assert_called_with(
+            2.0,
+            service_name=None,
+            endpoint_override='http://example.com',
+            region_name='region-al',
+            interface='public',
+            session=mock.ANY,
+            # Not a typo - the config dict above overrides this
+            service_type='mage'
+        )
+
+    @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
+    def test_legacy_client_image_unversioned(self, mock_get_session_endpoint):
+        mock_client = mock.Mock()
+        mock_get_session_endpoint.return_value = 'http://example.com/'
+        config_dict = defaults.get_defaults()
+        config_dict.update(fake_services_dict)
+        # Versionless endpoint, config wins
+        config_dict['image_api_version'] = '1'
+        cc = cloud_config.CloudConfig(
+            "test1", "region-al", config_dict, auth_plugin=mock.Mock())
+        cc.get_legacy_client('image', mock_client)
+        mock_client.assert_called_with(
+            '1',
+            service_name=None,
+            endpoint_override='http://example.com',
+            region_name='region-al',
+            interface='public',
+            session=mock.ANY,
+            # Not a typo - the config dict above overrides this
+            service_type='mage'
+        )
+
+    @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
+    def test_legacy_client_image_argument(self, mock_get_session_endpoint):
+        mock_client = mock.Mock()
+        mock_get_session_endpoint.return_value = 'http://example.com/v3'
+        config_dict = defaults.get_defaults()
+        config_dict.update(fake_services_dict)
+        # Versionless endpoint, config wins
+        config_dict['image_api_version'] = '6'
+        cc = cloud_config.CloudConfig(
+            "test1", "region-al", config_dict, auth_plugin=mock.Mock())
+        cc.get_legacy_client('image', mock_client, version='beef')
+        mock_client.assert_called_with(
+            'beef',
+            service_name=None,
+            endpoint_override='http://example.com',
             region_name='region-al',
             interface='public',
             session=mock.ANY,

From 8fa55700b90e335e54cd459ea8a60578e8d27fc7 Mon Sep 17 00:00:00 2001
From: Clark Boylan <clark.boylan@gmail.com>
Date: Tue, 22 Dec 2015 12:28:20 -0800
Subject: [PATCH 19/39] If cloud doesn't list regions expand passed name

Don't fail on a cloud not having regions when a region name is passed.
Instead just use the name that is given and expand it properly.

This adds test coverage for the paths through the
OpenStackConfig._get_region() method to avoid problems like this in the
future.

In order for this work to be done cleanly a small refactor of
get_regions() is done to split it into two methods, one that gets all
regions with a sane fallback default (for backward compat) and another
that returns only regions that are known in the config and None
otherwise. This allows us to switch on whether or not there are known
regions.

Change-Id: I62736ea82f365badaea5016a23d37a9f1c760927
---
 os_client_config/config.py            | 15 +++++--
 os_client_config/tests/base.py        | 10 ++++-
 os_client_config/tests/test_config.py | 56 +++++++++++++++++++++++++++
 3 files changed, 76 insertions(+), 5 deletions(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 077c109..89015cc 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -366,6 +366,13 @@ class OpenStackConfig(object):
     def _get_regions(self, cloud):
         if cloud not in self.cloud_config['clouds']:
             return [self._expand_region_name('')]
+        regions = self._get_known_regions(cloud)
+        if not regions:
+            # We don't know of any regions use a workable default.
+            regions = [self._expand_region_name('')]
+        return regions
+
+    def _get_known_regions(self, cloud):
         config = self._normalize_keys(self.cloud_config['clouds'][cloud])
         if 'regions' in config:
             return self._expand_regions(config['regions'])
@@ -386,15 +393,15 @@ class OpenStackConfig(object):
                 return self._expand_regions(new_cloud['regions'])
             elif 'region_name' in new_cloud and new_cloud['region_name']:
                 return [self._expand_region_name(new_cloud['region_name'])]
-            else:
-                # Wow. We really tried
-                return [self._expand_region_name('')]
 
     def _get_region(self, cloud=None, region_name=''):
         if not cloud:
             return self._expand_region_name(region_name)
 
-        regions = self._get_regions(cloud)
+        regions = self._get_known_regions(cloud)
+        if not regions:
+            return self._expand_region_name(region_name)
+
         if not region_name:
             return regions[0]
 
diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py
index fdc50cd..3f00f6d 100644
--- a/os_client_config/tests/base.py
+++ b/os_client_config/tests/base.py
@@ -119,7 +119,15 @@ USER_CONF = {
                 'auth_url': 'http://example.com/v2',
             },
             'region_name': 'test-region',
-        }
+        },
+        '_test-cloud_no_region': {
+            'profile': '_test_cloud_in_our_cloud',
+            'auth': {
+                'auth_url': 'http://example.com/v2',
+                'username': 'testuser',
+                'password': 'testpass',
+            },
+        },
     },
     'ansible': {
         'expand-hostvars': False,
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index 30ed731..3ea6690 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -178,6 +178,7 @@ class TestConfig(base.TestCase):
             ['_test-cloud-domain-id_',
              '_test-cloud-int-project_',
              '_test-cloud_',
+             '_test-cloud_no_region',
              '_test_cloud_hyphenated',
              '_test_cloud_no_vendor',
              '_test_cloud_regions',
@@ -230,6 +231,61 @@ class TestConfig(base.TestCase):
             written_config['cache'].pop('path', None)
             self.assertEqual(written_config, resulting_config)
 
+    def test_get_region_no_region_default(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test-cloud_no_region')
+        self.assertEqual(region, {'name': '', 'values': {}})
+
+    def test_get_region_no_region(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test-cloud_no_region',
+                               region_name='override-region')
+        self.assertEqual(region, {'name': 'override-region', 'values': {}})
+
+    def test_get_region_region_set(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test-cloud_', region_name='test-region')
+        self.assertEqual(region, {'name': 'test-region', 'values': {}})
+
+    def test_get_region_many_regions_default(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test_cloud_regions',
+                               region_name='')
+        self.assertEqual(region, {'name': 'region1', 'values':
+                         {'external_network': 'region1-network'}})
+
+    def test_get_region_many_regions(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test_cloud_regions',
+                               region_name='region2')
+        self.assertEqual(region, {'name': 'region2', 'values':
+                         {'external_network': 'my-network'}})
+
+    def test_get_region_invalid_region(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        self.assertRaises(
+            exceptions.OpenStackConfigException, c._get_region,
+            cloud='_test_cloud_regions', region_name='invalid-region')
+
+    def test_get_region_no_cloud(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(region_name='no-cloud-region')
+        self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}})
+
 
 class TestConfigArgparse(base.TestCase):
 

From f765a16c6c84d8b4a116585676f46087e458987b Mon Sep 17 00:00:00 2001
From: Doug Hellmann <doug@doughellmann.com>
Date: Wed, 23 Dec 2015 01:31:13 +0000
Subject: [PATCH 20/39] remove python 2.6 os-client-config classifier

OpenStack projects are no longer being tested under Python 2.6, so
remove the classifier implying that this project supports 2.6.

Change-Id: Ic24f93d5f7e7ffb1eaf91617c09cc897163e88df
---
 setup.cfg | 1 -
 1 file changed, 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index bc4f128..89df35c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,7 +15,6 @@ classifier =
     Programming Language :: Python
     Programming Language :: Python :: 2
     Programming Language :: Python :: 2.7
-    Programming Language :: Python :: 2.6
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.3
     Programming Language :: Python :: 3.4

From 77c0365ce2d42adce9352cf238fbdbc7c282222f Mon Sep 17 00:00:00 2001
From: Javier Pena <jpena@redhat.com>
Date: Wed, 23 Dec 2015 11:38:40 +0100
Subject: [PATCH 21/39] Fix token_endpoint usage

Commit 22d740b7007e1182c99370cb2629322384b17a14 broke token_endpoint
authentication for openstackclient, by unconditionally setting
auth_type to 'token' whenever a token was passed in the command line.

This change reverts the portion that always overrides the auth plugin
if there is a token passed via arguments.

Change-Id: I835c3716dd08eaca10f56682c22fdc6ac700e0fe
---
 os_client_config/config.py            | 1 -
 os_client_config/tests/test_config.py | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 89015cc..5101983 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -883,7 +883,6 @@ class OpenStackConfig(object):
         if (('auth' in config and 'token' in config['auth']) or
                 ('auth_token' in config and config['auth_token']) or
                 ('token' in config and config['token'])):
-            config['auth_type'] = 'token'
             config.setdefault('token', config.pop('auth_token', None))
 
         # These backwards compat values are only set via argparse. If it's
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index 3ea6690..aef9737 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -482,7 +482,8 @@ class TestConfigArgparse(base.TestCase):
         # novaclient will add this
         parser.add_argument('--os-auth-token')
         opts, _remain = parser.parse_known_args(
-            ['--os-auth-token', 'very-bad-things'])
+            ['--os-auth-token', 'very-bad-things',
+             '--os-auth-type', 'token'])
         cc = c.get_one_cloud(argparse=opts)
         self.assertEqual(cc.config['auth_type'], 'token')
         self.assertEqual(cc.config['auth']['token'], 'very-bad-things')

From 7be6db82d35a7c3402172e742bb19dfa5a95a472 Mon Sep 17 00:00:00 2001
From: Tim Burke <tim.burke@gmail.com>
Date: Mon, 28 Dec 2015 16:43:26 -0800
Subject: [PATCH 22/39] Fix some README typos

Change-Id: I3ebec661d1b02da0c940cde63ab862871dca11c5
---
 README.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.rst b/README.rst
index ced3b18..811c2d3 100644
--- a/README.rst
+++ b/README.rst
@@ -117,7 +117,7 @@ An example config file is probably helpful:
       - IAD
 
 You may note a few things. First, since `auth_url` settings are silly
-and embarrasingly ugly, known cloud vendor profile information is included and
+and embarrassingly ugly, known cloud vendor profile information is included and
 may be referenced by name. One of the benefits of that is that `auth_url`
 isn't the only thing the vendor defaults contain. For instance, since
 Rackspace lists `rax:database` as the service type for trove, `os-client-config`
@@ -148,8 +148,8 @@ related to domains, projects and trusts.
 Splitting Secrets
 -----------------
 
-In some scenarios, such as configuragtion managment controlled environments,
-it might be eaiser to have secrets in one file and non-secrets in another.
+In some scenarios, such as configuration management controlled environments,
+it might be easier to have secrets in one file and non-secrets in another.
 This is fully supported via an optional file `secure.yaml` which follows all
 the same location rules as `clouds.yaml`. It can contain anything you put
 in `clouds.yaml` and will take precedence over anything in the `clouds.yaml`
@@ -380,4 +380,4 @@ If you want to do the same thing but also support command line parsing.
       'compute', options=argparse.ArgumentParser())
 
 If you want to get fancier than that in your python, then the rest of the
-API is avaiable to you. But often times, you just want to do the one thing.
+API is available to you. But often times, you just want to do the one thing.

From 17e019a08e6e8fed7da6d0de403e5525d997095b Mon Sep 17 00:00:00 2001
From: Colleen Murphy <colleen@gazlene.net>
Date: Tue, 29 Dec 2015 15:22:56 -0800
Subject: [PATCH 23/39] Munge region_name to '' if set to None

The openstack ansible module defaults to setting region_name to
None[1]. With region_name explicitly set, _get_region won't use '' as a
default and therefor has unexpected behavior if the user does not set
the region explicitly. This is apparent in bifrost[2] which does not
use any cloud config file and does not set the region explicitly. This
patch checks whether None was passed in as the region name and sets it
to '' so that it can continue processing it as though it was not set.

[1] https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/openstack.py#L41
[2] http://paste.openstack.org/show/482831/

Change-Id: I22cce104930f74dd479e704cc1a941dc945b75de
---
 os_client_config/config.py            | 2 ++
 os_client_config/tests/test_config.py | 7 +++++++
 2 files changed, 9 insertions(+)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 89015cc..d5b1ab5 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -395,6 +395,8 @@ class OpenStackConfig(object):
                 return [self._expand_region_name(new_cloud['region_name'])]
 
     def _get_region(self, cloud=None, region_name=''):
+        if region_name is None:
+            region_name = ''
         if not cloud:
             return self._expand_region_name(region_name)
 
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index 3ea6690..b2ee9bb 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -246,6 +246,13 @@ class TestConfig(base.TestCase):
                                region_name='override-region')
         self.assertEqual(region, {'name': 'override-region', 'values': {}})
 
+    def test_get_region_region_is_none(self):
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml],
+                                   secure_files=[self.no_yaml])
+        region = c._get_region(cloud='_test-cloud_no_region', region_name=None)
+        self.assertEqual(region, {'name': '', 'values': {}})
+
     def test_get_region_region_set(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml],

From 7ee7156254381dc5c06405105c7de42c180c779f Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 30 Dec 2015 09:46:21 -0600
Subject: [PATCH 24/39] Allow filtering clouds on command line

Add a very basic filtering to the test command line function to allow
only printing one cloud or one cloud/region worth of config.

Change-Id: I0d09717430f41b4229f7743f8531f871b962969e
---
 os_client_config/config.py | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index 077c109..b970728 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -19,6 +19,7 @@ import collections
 import copy
 import json
 import os
+import sys
 import warnings
 
 import appdirs
@@ -976,4 +977,15 @@ class OpenStackConfig(object):
 if __name__ == '__main__':
     config = OpenStackConfig().get_all_clouds()
     for cloud in config:
-        print(cloud.name, cloud.region, cloud.config)
+        print_cloud = False
+        if len(sys.argv) == 1:
+            print_cloud = True
+        elif len(sys.argv) == 3 and (
+                sys.argv[1] == cloud.name and sys.argv[2] == cloud.region):
+            print_cloud = True
+        elif len(sys.argv) == 2 and (
+                sys.argv[1] == cloud.name):
+            print_cloud = True
+
+        if print_cloud:
+            print(cloud.name, cloud.region, cloud.config)

From f3678f03deac0230e1265a8a516a8eea11d301cf Mon Sep 17 00:00:00 2001
From: Doug Hellmann <doug@doughellmann.com>
Date: Wed, 30 Dec 2015 19:06:38 +0000
Subject: [PATCH 25/39] add URLs for release announcement tools

The release announcement scripts expects to find URLs for the bug
tracker, documentation, etc. by looking for patterns in the README.rst
file. This change adds the URLs in a format consistent with other
OpenStack projects and that works with the release announcement
generator.

Change-Id: I88151008cca91da3fed7e4c0ec6dfb641a0062b6
---
 README.rst | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/README.rst b/README.rst
index 811c2d3..5cbc118 100644
--- a/README.rst
+++ b/README.rst
@@ -381,3 +381,11 @@ If you want to do the same thing but also support command line parsing.
 
 If you want to get fancier than that in your python, then the rest of the
 API is available to you. But often times, you just want to do the one thing.
+
+Source
+------
+
+* Free software: Apache license
+* Documentation: http://docs.openstack.org/developer/os-client-config
+* Source: http://git.openstack.org/cgit/openstack/os-client-config
+* Bugs: http://bugs.launchpad.net/os-client-config

From 594e31a4c262c9ae3fe14e2e4c0fdb71a0df0747 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 30 Dec 2015 13:10:47 -0600
Subject: [PATCH 26/39] Use reno for release notes

The OpenStack Release team has created a great release notes management
tool that integrates with Sphinx. Start using it. For reference on how
to use it, see http://docs.openstack.org/developer/reno/

Change-Id: I8153ec7861b508297a28a1916771776dee2deafe
---
 doc/source/conf.py                                          | 3 ++-
 doc/source/index.rst                                        | 1 +
 doc/source/releasenotes.rst                                 | 5 +++++
 releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml | 3 +++
 test-requirements.txt                                       | 1 +
 5 files changed, 12 insertions(+), 1 deletion(-)
 create mode 100644 doc/source/releasenotes.rst
 create mode 100644 releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml

diff --git a/doc/source/conf.py b/doc/source/conf.py
index 221de3c..208517c 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -23,7 +23,8 @@ sys.path.insert(0, os.path.abspath('../..'))
 extensions = [
     'sphinx.ext.autodoc',
     #'sphinx.ext.intersphinx',
-    'oslosphinx'
+    'oslosphinx',
+    'reno.sphinxext'
 ]
 
 # autodoc generation is a bit aggressive and a nuisance when doing heavy
diff --git a/doc/source/index.rst b/doc/source/index.rst
index cc5dbf4..bf667b7 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -7,6 +7,7 @@
    contributing
    installation
    api-reference
+   releasenotes
 
 Indices and tables
 ==================
diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst
new file mode 100644
index 0000000..2a4bceb
--- /dev/null
+++ b/doc/source/releasenotes.rst
@@ -0,0 +1,5 @@
+=============
+Release Notes
+=============
+
+.. release-notes::
diff --git a/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml
new file mode 100644
index 0000000..d7cfb51
--- /dev/null
+++ b/releasenotes/notes/started-using-reno-242e2b0cd27f9480.yaml
@@ -0,0 +1,3 @@
+---
+other:
+- Started using reno for release notes.
diff --git a/test-requirements.txt b/test-requirements.txt
index 7053051..a50a202 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -16,6 +16,7 @@ python-subunit>=0.0.18
 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
 oslosphinx>=2.5.0,<2.6.0  # Apache-2.0
 oslotest>=1.5.1,<1.6.0  # Apache-2.0
+reno>=0.1.1  # Apache2
 testrepository>=0.0.18
 testscenarios>=0.4
 testtools>=0.9.36,!=1.2.0

From 9688f8ebd1ace0f338a1eabb77e1bee249e5630b Mon Sep 17 00:00:00 2001
From: Yuriy Taraday <yorik.sar@gmail.com>
Date: Thu, 31 Dec 2015 15:40:58 +0300
Subject: [PATCH 27/39] Fix README.rst, add a check for it to fit PyPI rules

README.rst doesn't appear right on PyPI currently. This commit fixes the
issue and expands "docs" environment in tox.ini to use readme tool [0]
to verify that README.rst is good for PyPI.

[0] https://github.com/pypa/readme

Change-Id: I6025bb6c661d8a4a7cd9802a1298928662278f2d
---
 README.rst | 2 +-
 tox.ini    | 7 ++++++-
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/README.rst b/README.rst
index 5cbc118..f078f3c 100644
--- a/README.rst
+++ b/README.rst
@@ -355,7 +355,7 @@ with - as well as a consumption argument.
 Constructing Legacy Client objects
 ----------------------------------
 
-If all you want to do is get a Client object from a python-*client library,
+If all you want to do is get a Client object from a python-\*client library,
 and you want it to do all the normal things related to clouds.yaml, `OS_`
 environment variables, a helper function is provided. The following
 will get you a fully configured `novaclient` instance.
diff --git a/tox.ini b/tox.ini
index 7a2d3a0..95dff6b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,12 @@ commands = {posargs}
 commands = python setup.py test --coverage --coverage-package-name=os_client_config --testr-args='{posargs}'
 
 [testenv:docs]
-commands = python setup.py build_sphinx
+deps =
+    {[testenv]deps}
+    readme
+commands =
+    python setup.py build_sphinx
+    python setup.py check -r -s
 
 [flake8]
 # H803 skipped on purpose per list discussion.

From c514b855d1faed8947ace885bb4656da541d4d2b Mon Sep 17 00:00:00 2001
From: Doug Wiegley <doug@parksidesoftware.com>
Date: Thu, 31 Dec 2015 12:32:37 -0700
Subject: [PATCH 28/39] Debug log a deferred keystone exception, else we mask
 some useful diag

Change-Id: Ib1921698bb61f44193034065749b4e246a6258db
---
 os_client_config/config.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index b572645..d490006 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -26,6 +26,7 @@ from keystoneauth1 import adapter
 from keystoneauth1 import loading
 import yaml
 
+from os_client_config import _log
 from os_client_config import cloud_config
 from os_client_config import defaults
 from os_client_config import exceptions
@@ -170,6 +171,8 @@ class OpenStackConfig(object):
     def __init__(self, config_files=None, vendor_files=None,
                  override_defaults=None, force_ipv4=None,
                  envvar_prefix=None, secure_files=None):
+        self.log = _log.setup_logging(__name__)
+
         self._config_files = config_files or CONFIG_FILES
         self._secure_files = secure_files or SECURE_FILES
         self._vendor_files = vendor_files or VENDOR_FILES
@@ -920,6 +923,7 @@ class OpenStackConfig(object):
                 # but OSC can't handle it right now, so we try deferring
                 # to ksc. If that ALSO fails, it means there is likely
                 # a deeper issue, so we assume the ksa error was correct
+                self.log.debug("Deferring keystone exception: {e}".format(e=e))
                 auth_plugin = None
                 try:
                     config = self._validate_auth_ksc(config)

From 1cd3e5bb7fd7cd72a481f5ae8bbcd0b2ab114680 Mon Sep 17 00:00:00 2001
From: Yaguang Tang <yaguang@umcloud.com>
Date: Sun, 27 Dec 2015 10:59:08 +0800
Subject: [PATCH 29/39] Update volume API default version from v1 to v2

Cinder has deprecated API version v1 since Juno release, and
there is a blueprint to remove v1 API support which is in progress.
We should default to v2 API when it's there.

Closes-Bug: 1467589
Change-Id: I83aef4c681cbe342c445f02436fcd40cf1222f23
---
 doc/source/vendor-support.rst                 | 11 +++++++++++
 os_client_config/defaults.json                |  2 +-
 os_client_config/vendors/bluebox.json         |  1 +
 os_client_config/vendors/catalyst.json        |  1 +
 os_client_config/vendors/citycloud.json       |  1 +
 os_client_config/vendors/entercloudsuite.json |  1 +
 os_client_config/vendors/hp.json              |  1 +
 os_client_config/vendors/rackspace.json       |  1 +
 os_client_config/vendors/switchengines.json   |  1 +
 os_client_config/vendors/ultimum.json         |  1 +
 os_client_config/vendors/unitedstack.json     |  1 +
 11 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst
index d7af6b9..8ae2f61 100644
--- a/doc/source/vendor-support.rst
+++ b/doc/source/vendor-support.rst
@@ -16,6 +16,7 @@ These are the default behaviors unless a cloud is configured differently.
 * Identity uses `password` authentication
 * Identity API Version is 2
 * Image API Version is 2
+* Volume API Version is 2
 * Images must be in `qcow2` format
 * Images are uploaded using PUT interface
 * Public IPv4 is directly routable via DHCP from Neutron
@@ -51,6 +52,7 @@ nz_wlg_2       Wellington, NZ
 
 * Image API Version is 1
 * Images must be in `raw` format
+* Volume API Version is 1
 
 citycloud
 ---------
@@ -67,6 +69,7 @@ Kna1           Karlskrona, SE
 
 * Identity API Version is 3
 * Public IPv4 is provided via NAT with Neutron Floating IP
+* Volume API Version is 1
 
 conoha
 ------
@@ -137,6 +140,8 @@ it-mil1        Milan, IT
 de-fra1        Frankfurt, DE
 ============== ================
 
+* Volume API Version is 1
+
 hp
 --
 
@@ -152,6 +157,7 @@ region-b.geo-1 US East
 * DNS Service Type is `hpext:dns`
 * Image API Version is 1
 * Public IPv4 is provided via NAT with Neutron Floating IP
+* Volume API Version is 1
 
 internap
 --------
@@ -212,6 +218,7 @@ SYD            Sydney
 * Uploaded Images need properties to not use vendor agent::
   :vm_mode: hvm
   :xenapi_use_agent: False
+* Volume API Version is 1
 
 runabove
 --------
@@ -241,6 +248,7 @@ ZH             Zurich, CH
 
 * Images must be in `raw` format
 * Images must be uploaded using the Glance Task Interface
+* Volume API Version is 1
 
 ultimum
 -------
@@ -253,6 +261,8 @@ Region Name    Human Name
 RegionOne      Region One
 ============== ================
 
+* Volume API Version is 1
+
 unitedstack
 -----------
 
@@ -267,6 +277,7 @@ gd1            Guangdong
 
 * Identity API Version is 3
 * Images must be in `raw` format
+* Volume API Version is 1
 
 vexxhost
 --------
diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json
index 6735b55..2ffb672 100644
--- a/os_client_config/defaults.json
+++ b/os_client_config/defaults.json
@@ -17,5 +17,5 @@
   "object_store_api_version": "1",
   "orchestration_api_version": "1",
   "secgroup_source": "neutron",
-  "volume_api_version": "1"
+  "volume_api_version": "2"
 }
diff --git a/os_client_config/vendors/bluebox.json b/os_client_config/vendors/bluebox.json
index 2227aac..647c842 100644
--- a/os_client_config/vendors/bluebox.json
+++ b/os_client_config/vendors/bluebox.json
@@ -1,6 +1,7 @@
 {
   "name": "bluebox",
   "profile": {
+    "volume_api_version": "1",
     "region_name": "RegionOne"
   }
 }
diff --git a/os_client_config/vendors/catalyst.json b/os_client_config/vendors/catalyst.json
index ddde838..3ad7507 100644
--- a/os_client_config/vendors/catalyst.json
+++ b/os_client_config/vendors/catalyst.json
@@ -9,6 +9,7 @@
       "nz_wlg_2"
     ],
     "image_api_version": "1",
+    "volume_api_version": "1",
     "image_format": "raw"
   }
 }
diff --git a/os_client_config/vendors/citycloud.json b/os_client_config/vendors/citycloud.json
index f6c57c7..64cadce 100644
--- a/os_client_config/vendors/citycloud.json
+++ b/os_client_config/vendors/citycloud.json
@@ -9,6 +9,7 @@
       "Sto2",
       "Kna1"
     ],
+    "volume_api_version": "1",
     "identity_api_version": "3"
   }
 }
diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json
index 826c25f..5a425b4 100644
--- a/os_client_config/vendors/entercloudsuite.json
+++ b/os_client_config/vendors/entercloudsuite.json
@@ -4,6 +4,7 @@
     "auth": {
       "auth_url": "https://api.entercloudsuite.com/v2.0"
     },
+    "volume_api_version": "1",
     "regions": [
       "it-mil1",
       "nl-ams1",
diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json
index 10789a9..ac280f2 100644
--- a/os_client_config/vendors/hp.json
+++ b/os_client_config/vendors/hp.json
@@ -9,6 +9,7 @@
       "region-b.geo-1"
     ],
     "dns_service_type": "hpext:dns",
+    "volume_api_version": "1",
     "image_api_version": "1"
   }
 }
diff --git a/os_client_config/vendors/rackspace.json b/os_client_config/vendors/rackspace.json
index 582e122..3fbbacd 100644
--- a/os_client_config/vendors/rackspace.json
+++ b/os_client_config/vendors/rackspace.json
@@ -18,6 +18,7 @@
     "image_format": "vhd",
     "floating_ip_source": "None",
     "secgroup_source": "None",
+    "volume_api_version": "1",
     "disable_vendor_agent": {
       "vm_mode": "hvm",
       "xenapi_use_agent": false
diff --git a/os_client_config/vendors/switchengines.json b/os_client_config/vendors/switchengines.json
index 8a7c566..46f6325 100644
--- a/os_client_config/vendors/switchengines.json
+++ b/os_client_config/vendors/switchengines.json
@@ -8,6 +8,7 @@
       "LS",
       "ZH"
     ],
+    "volume_api_version": "1",
     "image_api_use_tasks": true,
     "image_format": "raw"
   }
diff --git a/os_client_config/vendors/ultimum.json b/os_client_config/vendors/ultimum.json
index ada6e3d..0b38d71 100644
--- a/os_client_config/vendors/ultimum.json
+++ b/os_client_config/vendors/ultimum.json
@@ -4,6 +4,7 @@
     "auth": {
       "auth_url": "https://console.ultimum-cloud.com:5000/v2.0"
     },
+    "volume_api_version": "1",
     "region-name": "RegionOne"
   }
 }
diff --git a/os_client_config/vendors/unitedstack.json b/os_client_config/vendors/unitedstack.json
index 41f4585..ac8be11 100644
--- a/os_client_config/vendors/unitedstack.json
+++ b/os_client_config/vendors/unitedstack.json
@@ -8,6 +8,7 @@
       "bj1",
       "gd1"
     ],
+    "volume_api_version": "1",
     "identity_api_version": "3",
     "image_format": "raw",
     "floating_ip_source": "None"

From 0bc9e33c9f978a8262453d7364143e8a02d3eded Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 6 Jan 2016 08:43:19 -0600
Subject: [PATCH 30/39] Stop hardcoding compute in simple_client

There's a debug leftover oops where we just passed 'compute' rather than
the service_key requested.

Change-Id: Id8c82e43ba34859426b1fdc93dcf3ab2bbde4966
---
 os_client_config/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py
index ac585f2..ece1559 100644
--- a/os_client_config/__init__.py
+++ b/os_client_config/__init__.py
@@ -31,7 +31,7 @@ def simple_client(service_key, cloud=None, region_name=None):
     at OpenStack REST APIs with a properly configured keystone session.
     """
     return OpenStackConfig().get_one_cloud(
-        cloud=cloud, region_name=region_name).get_session_client('compute')
+        cloud=cloud, region_name=region_name).get_session_client(service_key)
 
 
 def make_client(service_key, constructor, options=None, **kwargs):

From 3b5673ce4c8c9d54568028056300eae053828ee0 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Tue, 5 Jan 2016 10:30:28 -0600
Subject: [PATCH 31/39] Update auth urls and identity API versions

Most of the clouds, it turns out, support unversioned auth_url as well
as keystone v3.

Change-Id: I088d008cd2732f137c8a1bbbd9c0a43f7d382f92
---
 doc/source/vendor-support.rst                 | 4 ++--
 os_client_config/vendors/auro.json            | 1 +
 os_client_config/vendors/conoha.json          | 5 +++--
 os_client_config/vendors/datacentred.json     | 3 ++-
 os_client_config/vendors/dreamhost.json       | 3 ++-
 os_client_config/vendors/elastx.json          | 3 ++-
 os_client_config/vendors/entercloudsuite.json | 3 ++-
 os_client_config/vendors/hp.json              | 3 ++-
 os_client_config/vendors/internap.json        | 3 ++-
 os_client_config/vendors/ovh.json             | 3 ++-
 os_client_config/vendors/runabove.json        | 3 ++-
 os_client_config/vendors/ultimum.json         | 3 ++-
 12 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst
index 8ae2f61..a215822 100644
--- a/doc/source/vendor-support.rst
+++ b/doc/source/vendor-support.rst
@@ -74,7 +74,7 @@ Kna1           Karlskrona, SE
 conoha
 ------
 
-https://identity.%(region_name)s.conoha.io/v2.0
+https://identity.%(region_name)s.conoha.io
 
 ============== ================
 Region Name    Human Name
@@ -89,7 +89,7 @@ sjc1           San Jose, CA
 datacentred
 -----------
 
-https://compute.datacentred.io:5000/v2.0
+https://compute.datacentred.io:5000
 
 ============== ================
 Region Name    Human Name
diff --git a/os_client_config/vendors/auro.json b/os_client_config/vendors/auro.json
index 1e59f01..a9e709b 100644
--- a/os_client_config/vendors/auro.json
+++ b/os_client_config/vendors/auro.json
@@ -4,6 +4,7 @@
     "auth": {
       "auth_url": "https://api.van1.auro.io:5000/v2.0"
     },
+    "identity_api_version": "2",
     "region_name": "van1"
   }
 }
diff --git a/os_client_config/vendors/conoha.json b/os_client_config/vendors/conoha.json
index 8e33ca4..5636f09 100644
--- a/os_client_config/vendors/conoha.json
+++ b/os_client_config/vendors/conoha.json
@@ -2,12 +2,13 @@
   "name": "conoha",
   "profile": {
     "auth": {
-      "auth_url": "https://identity.{region_name}.conoha.io/v2.0"
+      "auth_url": "https://identity.{region_name}.conoha.io"
     },
     "regions": [
       "sin1",
       "sjc1",
       "tyo1"
-    ]
+    ],
+    "identity_api_version": "2"
   }
 }
diff --git a/os_client_config/vendors/datacentred.json b/os_client_config/vendors/datacentred.json
index 1fb4dbb..2be4a58 100644
--- a/os_client_config/vendors/datacentred.json
+++ b/os_client_config/vendors/datacentred.json
@@ -2,9 +2,10 @@
   "name": "datacentred",
   "profile": {
     "auth": {
-      "auth_url": "https://compute.datacentred.io:5000/v2.0"
+      "auth_url": "https://compute.datacentred.io:5000"
     },
     "region-name": "sal01",
+    "identity_api_version": "2",
     "image_api_version": "1"
   }
 }
diff --git a/os_client_config/vendors/dreamhost.json b/os_client_config/vendors/dreamhost.json
index 8580826..6fc2ccf 100644
--- a/os_client_config/vendors/dreamhost.json
+++ b/os_client_config/vendors/dreamhost.json
@@ -2,8 +2,9 @@
   "name": "dreamhost",
   "profile": {
     "auth": {
-      "auth_url": "https://keystone.dream.io/v2.0"
+      "auth_url": "https://keystone.dream.io"
     },
+    "identity_api_version": "3",
     "region_name": "RegionOne",
     "image_format": "raw"
   }
diff --git a/os_client_config/vendors/elastx.json b/os_client_config/vendors/elastx.json
index cac755e..1e72482 100644
--- a/os_client_config/vendors/elastx.json
+++ b/os_client_config/vendors/elastx.json
@@ -2,8 +2,9 @@
   "name": "elastx",
   "profile": {
     "auth": {
-      "auth_url": "https://ops.elastx.net:5000/v2.0"
+      "auth_url": "https://ops.elastx.net:5000"
     },
+    "identity_api_version": "3",
     "region_name": "regionOne"
   }
 }
diff --git a/os_client_config/vendors/entercloudsuite.json b/os_client_config/vendors/entercloudsuite.json
index 5a425b4..6d2fc12 100644
--- a/os_client_config/vendors/entercloudsuite.json
+++ b/os_client_config/vendors/entercloudsuite.json
@@ -2,8 +2,9 @@
   "name": "entercloudsuite",
   "profile": {
     "auth": {
-      "auth_url": "https://api.entercloudsuite.com/v2.0"
+      "auth_url": "https://api.entercloudsuite.com/"
     },
+    "identity_api_version": "3",
     "volume_api_version": "1",
     "regions": [
       "it-mil1",
diff --git a/os_client_config/vendors/hp.json b/os_client_config/vendors/hp.json
index ac280f2..b06b90a 100644
--- a/os_client_config/vendors/hp.json
+++ b/os_client_config/vendors/hp.json
@@ -2,12 +2,13 @@
   "name": "hp",
   "profile": {
     "auth": {
-      "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0"
+      "auth_url": "https://region-b.geo-1.identity.hpcloudsvc.com:35357"
     },
     "regions": [
       "region-a.geo-1",
       "region-b.geo-1"
     ],
+    "identity_api_version": "3",
     "dns_service_type": "hpext:dns",
     "volume_api_version": "1",
     "image_api_version": "1"
diff --git a/os_client_config/vendors/internap.json b/os_client_config/vendors/internap.json
index 9b27536..d5ad49f 100644
--- a/os_client_config/vendors/internap.json
+++ b/os_client_config/vendors/internap.json
@@ -2,13 +2,14 @@
   "name": "internap",
   "profile": {
     "auth": {
-      "auth_url": "https://identity.api.cloud.iweb.com/v2.0"
+      "auth_url": "https://identity.api.cloud.iweb.com"
     },
     "regions": [
       "ams01",
       "da01",
       "nyj01"
     ],
+    "identity_api_version": "3",
     "image_api_version": "1",
     "floating_ip_source": "None"
   }
diff --git a/os_client_config/vendors/ovh.json b/os_client_config/vendors/ovh.json
index 032741f..664f161 100644
--- a/os_client_config/vendors/ovh.json
+++ b/os_client_config/vendors/ovh.json
@@ -2,13 +2,14 @@
   "name": "ovh",
   "profile": {
     "auth": {
-      "auth_url": "https://auth.cloud.ovh.net/v2.0"
+      "auth_url": "https://auth.cloud.ovh.net/"
     },
     "regions": [
       "BHS1",
       "GRA1",
       "SBG1"
     ],
+    "identity_api_version": "3",
     "image_format": "raw",
     "floating_ip_source": "None"
   }
diff --git a/os_client_config/vendors/runabove.json b/os_client_config/vendors/runabove.json
index 56dd945..abf1116 100644
--- a/os_client_config/vendors/runabove.json
+++ b/os_client_config/vendors/runabove.json
@@ -2,12 +2,13 @@
   "name": "runabove",
   "profile": {
     "auth": {
-      "auth_url": "https://auth.runabove.io/v2.0"
+      "auth_url": "https://auth.runabove.io/"
     },
     "regions": [
       "BHS-1",
       "SBG-1"
     ],
+    "identity_api_version": "3",
     "image_format": "qcow2",
     "floating_ip_source": "None"
   }
diff --git a/os_client_config/vendors/ultimum.json b/os_client_config/vendors/ultimum.json
index 0b38d71..4bfd088 100644
--- a/os_client_config/vendors/ultimum.json
+++ b/os_client_config/vendors/ultimum.json
@@ -2,8 +2,9 @@
   "name": "ultimum",
   "profile": {
     "auth": {
-      "auth_url": "https://console.ultimum-cloud.com:5000/v2.0"
+      "auth_url": "https://console.ultimum-cloud.com:5000/"
     },
+    "identity_api_version": "3",
     "volume_api_version": "1",
     "region-name": "RegionOne"
   }

From 0b270f0bc9f6dd31d9c17bcc4d49d15630ee999b Mon Sep 17 00:00:00 2001
From: LiuNanke <nanke.liu@easystack.cn>
Date: Wed, 6 Jan 2016 22:49:52 +0800
Subject: [PATCH 32/39] Replace assertEqual(None, *) with assertIsNone in tests

Replace assertEqual(None, *) with assertIsNone in tests to have
more clear messages in case of failure.

There have one more place should be modified.

Change-Id: I53a8f129db0108892b8377edce2dbf19b0b95f5d
Closes-bug: #1280522
---
 os_client_config/tests/test_cloud_config.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py
index 01581b1..9d60111 100644
--- a/os_client_config/tests/test_cloud_config.py
+++ b/os_client_config/tests/test_cloud_config.py
@@ -148,8 +148,7 @@ class TestCloudConfig(base.TestCase):
         self.assertEqual('volume', cc.get_service_type('volume'))
         self.assertEqual('http://compute.example.com',
                          cc.get_endpoint('compute'))
-        self.assertEqual(None,
-                         cc.get_endpoint('image'))
+        self.assertIsNone(cc.get_endpoint('image'))
         self.assertIsNone(cc.get_service_name('compute'))
         self.assertEqual('locks', cc.get_service_name('identity'))
 

From cab0469ec4471a5fe924d6049cbfcdf2ac0cdba4 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Tue, 5 Jan 2016 09:08:05 -0600
Subject: [PATCH 33/39] Add IBM Public Cloud

IBM Cloud has a public Openstack Cloud. We should support it.

Change-Id: If0bc29c41869494b2a4da944f7792cbe0f217f0e
---
 doc/source/vendor-support.rst          | 13 +++++++++++++
 os_client_config/vendors/ibmcloud.json | 13 +++++++++++++
 2 files changed, 26 insertions(+)
 create mode 100644 os_client_config/vendors/ibmcloud.json

diff --git a/doc/source/vendor-support.rst b/doc/source/vendor-support.rst
index a215822..46c95d8 100644
--- a/doc/source/vendor-support.rst
+++ b/doc/source/vendor-support.rst
@@ -159,6 +159,19 @@ region-b.geo-1 US East
 * Public IPv4 is provided via NAT with Neutron Floating IP
 * Volume API Version is 1
 
+ibmcloud
+--------
+
+https://identity.open.softlayer.com
+
+============== ================
+Region Name    Human Name
+============== ================
+london         London, UK
+============== ================
+
+* Public IPv4 is provided via NAT with Neutron Floating IP
+
 internap
 --------
 
diff --git a/os_client_config/vendors/ibmcloud.json b/os_client_config/vendors/ibmcloud.json
new file mode 100644
index 0000000..90962c6
--- /dev/null
+++ b/os_client_config/vendors/ibmcloud.json
@@ -0,0 +1,13 @@
+{
+  "name": "ibmcloud",
+  "profile": {
+    "auth": {
+      "auth_url": "https://identity.open.softlayer.com"
+    },
+    "volume_api_version": "2",
+    "identity_api_version": "3",
+    "regions": [
+      "london"
+    ]
+  }
+}

From caae8ad43487d5060d113d294c8d8862c7d3f788 Mon Sep 17 00:00:00 2001
From: LiuNanke <nanke.liu@easystack.cn>
Date: Thu, 7 Jan 2016 15:21:31 +0800
Subject: [PATCH 34/39] Remove openstack-common.conf

We don't sync from oslo-incubator, so don't need this
file any more.

Change-Id: Ia4acc67fe38c4a27a098c4da263265ed3742b7e7
---
 openstack-common.conf | 6 ------
 1 file changed, 6 deletions(-)
 delete mode 100644 openstack-common.conf

diff --git a/openstack-common.conf b/openstack-common.conf
deleted file mode 100644
index e8eb2aa..0000000
--- a/openstack-common.conf
+++ /dev/null
@@ -1,6 +0,0 @@
-[DEFAULT]
-
-# The list of modules to copy from oslo-incubator.git
-
-# The base module to hold the copy of openstack.common
-base=os_client_config
\ No newline at end of file

From 9835daf9f684556c5aed4834dc086e932788f9bc Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Fri, 8 Jan 2016 20:24:17 -0500
Subject: [PATCH 35/39] Add barbicanclient support

barbicanclient is a lovely client library, so we should add support
for make_legacy_client to doing the right things constructing a Client
object.

Change-Id: Idf015b1119ef76b951c195a6498cbb7a928d6e44
---
 os_client_config/cloud_config.py   | 7 +++++--
 os_client_config/constructors.json | 1 +
 os_client_config/defaults.json     | 1 +
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index f73da04..3b9bee9 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -305,7 +305,7 @@ class CloudConfig(object):
         endpoint_override = self.get_endpoint(service_key)
 
         if not interface_key:
-            if service_key == 'image':
+            if service_key in ('image', 'key-manager'):
                 interface_key = 'interface'
             else:
                 interface_key = 'endpoint_type'
@@ -348,7 +348,10 @@ class CloudConfig(object):
                 if 'endpoint' not in constructor_kwargs:
                     endpoint = self.get_session_endpoint('identity')
                     constructor_kwargs['endpoint'] = endpoint
-            constructor_args.append(version)
+            if service_key == 'key-manager':
+                constructor_kwargs['version'] = version
+            else:
+                constructor_args.append(version)
 
         return client_class(*constructor_args, **constructor_kwargs)
 
diff --git a/os_client_config/constructors.json b/os_client_config/constructors.json
index be44339..89c844c 100644
--- a/os_client_config/constructors.json
+++ b/os_client_config/constructors.json
@@ -3,6 +3,7 @@
     "database": "troveclient.client.Client",
     "identity": "keystoneclient.client.Client",
     "image": "glanceclient.Client",
+    "key-manager": "barbicanclient.client.Client",
     "metering": "ceilometerclient.client.Client",
     "network": "neutronclient.neutron.client.Client",
     "object-store": "swiftclient.client.Connection",
diff --git a/os_client_config/defaults.json b/os_client_config/defaults.json
index 2ffb672..f501862 100644
--- a/os_client_config/defaults.json
+++ b/os_client_config/defaults.json
@@ -12,6 +12,7 @@
   "image_api_use_tasks": false,
   "image_api_version": "2",
   "image_format": "qcow2",
+  "key_manager_api_version": "v1",
   "metering_api_version": "2",
   "network_api_version": "2",
   "object_store_api_version": "1",

From f61a487fa13c8292b9fd3ac103e1133ac05dbd26 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Fri, 8 Jan 2016 20:38:35 -0500
Subject: [PATCH 36/39] Use _get_client in make_client helper function

We have a capability to know what constructor is needed for make_client,
but we didn't plumb it in. Make sure that the only thing needed is:

  os_client_config.make_client('compute')

Change-Id: I02aa1c46fa7cdfdb1409f8e1232e364b5ba48cd2
---
 os_client_config/__init__.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py
index ece1559..52fcb85 100644
--- a/os_client_config/__init__.py
+++ b/os_client_config/__init__.py
@@ -14,6 +14,7 @@
 
 import sys
 
+from os_client_config import cloud_config
 from os_client_config.config import OpenStackConfig  # noqa
 
 
@@ -34,7 +35,7 @@ def simple_client(service_key, cloud=None, region_name=None):
         cloud=cloud, region_name=region_name).get_session_client(service_key)
 
 
-def make_client(service_key, constructor, options=None, **kwargs):
+def make_client(service_key, constructor=None, options=None, **kwargs):
     """Simple wrapper for getting a client instance from a client lib.
 
     OpenStack Client Libraries all have a fairly consistent constructor
@@ -44,6 +45,8 @@ def make_client(service_key, constructor, options=None, **kwargs):
     variables and clouds.yaml - and takes as **kwargs anything you'd expect
     to pass in.
     """
+    if not constructor:
+        constructor = cloud_config._get_client(service_key)
     config = OpenStackConfig()
     if options:
         config.register_argparse_options(options, sys.argv, service_key)
@@ -51,5 +54,5 @@ def make_client(service_key, constructor, options=None, **kwargs):
     else:
         parsed_options = None
 
-    cloud_config = config.get_one_cloud(options=parsed_options, **kwargs)
-    return cloud_config.get_legacy_client(service_key, constructor)
+    cloud = config.get_one_cloud(options=parsed_options, **kwargs)
+    return cloud.get_legacy_client(service_key, constructor)

From cd5f16cc4d78fde5a812e2715ee9db430760972f Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Fri, 8 Jan 2016 20:50:35 -0500
Subject: [PATCH 37/39] Pass version arg by name not position

Everyone except neutron has a first parameter called "version" - so we
can pass it by name. For neutron, add a workaround, becuase YAY people
being different.

Change-Id: Icfd92e5e31763ffccc1ff673298f89d1888941fe
---
 os_client_config/cloud_config.py            |  9 ++++-----
 os_client_config/tests/test_cloud_config.py | 18 +++++++++---------
 2 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 3b9bee9..85c6f2a 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -335,7 +335,6 @@ class CloudConfig(object):
                 constructor_kwargs['endpoint_override'] = endpoint
         constructor_kwargs.update(kwargs)
         constructor_kwargs[interface_key] = interface
-        constructor_args = []
         if pass_version_arg:
             if not version:
                 version = self.get_api_version(service_key)
@@ -348,12 +347,12 @@ class CloudConfig(object):
                 if 'endpoint' not in constructor_kwargs:
                     endpoint = self.get_session_endpoint('identity')
                     constructor_kwargs['endpoint'] = endpoint
-            if service_key == 'key-manager':
-                constructor_kwargs['version'] = version
+            if service_key == 'network':
+                constructor_kwargs['api_version'] = version
             else:
-                constructor_args.append(version)
+                constructor_kwargs['version'] = version
 
-        return client_class(*constructor_args, **constructor_kwargs)
+        return client_class(**constructor_kwargs)
 
     def _get_swift_client(self, client_class, **kwargs):
         session = self.get_session()
diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py
index 9d60111..a01d0e1 100644
--- a/os_client_config/tests/test_cloud_config.py
+++ b/os_client_config/tests/test_cloud_config.py
@@ -304,7 +304,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client)
         mock_client.assert_called_with(
-            2.0,
+            version=2.0,
             service_name=None,
             endpoint_override='http://example.com',
             region_name='region-al',
@@ -325,7 +325,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client)
         mock_client.assert_called_with(
-            2.0,
+            version=2.0,
             service_name=None,
             endpoint_override='http://example.com/override',
             region_name='region-al',
@@ -347,7 +347,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client)
         mock_client.assert_called_with(
-            2.0,
+            version=2.0,
             service_name=None,
             endpoint_override='http://example.com',
             region_name='region-al',
@@ -369,7 +369,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client)
         mock_client.assert_called_with(
-            '1',
+            version='1',
             service_name=None,
             endpoint_override='http://example.com',
             region_name='region-al',
@@ -391,7 +391,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('image', mock_client, version='beef')
         mock_client.assert_called_with(
-            'beef',
+            version='beef',
             service_name=None,
             endpoint_override='http://example.com',
             region_name='region-al',
@@ -411,7 +411,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('network', mock_client)
         mock_client.assert_called_with(
-            '2.0',
+            api_version='2.0',
             endpoint_type='public',
             endpoint_override=None,
             region_name='region-al',
@@ -429,7 +429,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('compute', mock_client)
         mock_client.assert_called_with(
-            '2',
+            version='2',
             endpoint_type='public',
             endpoint_override='http://compute.example.com',
             region_name='region-al',
@@ -447,7 +447,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('identity', mock_client)
         mock_client.assert_called_with(
-            '2.0',
+            version='2.0',
             endpoint='http://example.com/v2',
             endpoint_type='admin',
             endpoint_override=None,
@@ -467,7 +467,7 @@ class TestCloudConfig(base.TestCase):
             "test1", "region-al", config_dict, auth_plugin=mock.Mock())
         cc.get_legacy_client('identity', mock_client)
         mock_client.assert_called_with(
-            '3',
+            version='3',
             endpoint='http://example.com',
             endpoint_type='admin',
             endpoint_override=None,

From 7e5496763522475bb07a377359d69454f1942e1b Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Mon, 4 Jan 2016 12:56:28 -0600
Subject: [PATCH 38/39] Return empty dict instead of None for lack of file

We return None for the file content for non-existent files as a
fallback. This is normally fine, but in the case of a person having
_only_ a secure.conf file, this means that the dictionary merge fails.

Change-Id: I61cc0a8c709ea3510428fc3dfce63dc254c07c83
---
 os_client_config/config.py            | 2 +-
 os_client_config/tests/test_config.py | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index d490006..d366307 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -315,7 +315,7 @@ class OpenStackConfig(object):
                         return path, json.load(f)
                     else:
                         return path, yaml.safe_load(f)
-        return (None, None)
+        return (None, {})
 
     def _normalize_keys(self, config):
         new_config = {}
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index 4440ac8..dce436a 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -171,6 +171,13 @@ class TestConfig(base.TestCase):
         self.assertEqual('user', cc.auth['username'])
         self.assertEqual('testpass', cc.auth['password'])
 
+    def test_only_secure_yaml(self):
+        c = config.OpenStackConfig(config_files=['nonexistent'],
+                                   vendor_files=['nonexistent'],
+                                   secure_files=[self.secure_yaml])
+        cc = c.get_one_cloud(cloud='_test_cloud_no_vendor')
+        self.assertEqual('testpass', cc.auth['password'])
+
     def test_get_cloud_names(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    secure_files=[self.no_yaml])

From a8532f6c8d221628b697ddb0d134e2a000ef61d6 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Wed, 13 Jan 2016 13:37:14 -0500
Subject: [PATCH 39/39] Fix a precedence problem with auth arguments

With the current code, OS_TENANT_NAME will take precednece over
--os-project-name beause OS_TENANT_NAME gets early-moved to
config['auth']['project_name'], then when the argparse value gets put
into config['project_name'] the auth fixing sees auth['project_name']
and thinks it should win.

Change-Id: I97084ea221eb963f14d98cf550a04bbd5c7d954c
---
 os_client_config/config.py            |  4 +++-
 os_client_config/tests/test_config.py | 10 ++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/os_client_config/config.py b/os_client_config/config.py
index d366307..7e56f61 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -447,7 +447,7 @@ class OpenStackConfig(object):
         if 'cloud' in cloud:
             del cloud['cloud']
 
-        return self._fix_backwards_madness(cloud)
+        return cloud
 
     def _expand_vendor_profile(self, name, cloud, our_cloud):
         # Expand a profile if it exists. 'cloud' is an old confusing name
@@ -896,6 +896,8 @@ class OpenStackConfig(object):
         if 'endpoint_type' in config:
             config['interface'] = config.pop('endpoint_type')
 
+        config = self._fix_backwards_madness(config)
+
         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 dce436a..1a16bd8 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -473,6 +473,16 @@ class TestConfigArgparse(base.TestCase):
         opts, _remain = parser.parse_known_args(['--os-cloud', 'foo'])
         self.assertEqual(opts.os_cloud, 'foo')
 
+    def test_env_argparse_precedence(self):
+        self.useFixture(fixtures.EnvironmentVariable(
+            'OS_TENANT_NAME', 'tenants-are-bad'))
+        c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+                                   vendor_files=[self.vendor_yaml])
+
+        cc = c.get_one_cloud(
+            cloud='envvars', argparse=self.options)
+        self.assertEqual(cc.auth['project_name'], 'project')
+
     def test_argparse_default_no_token(self):
         c = config.OpenStackConfig(config_files=[self.cloud_yaml],
                                    vendor_files=[self.vendor_yaml])