From 8161ed81d6f9b89fc53ea260cfaa16003909271a Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Tue, 11 Jul 2017 17:20:45 -0500
Subject: [PATCH] Add min_version and max_version to adapter constructors

They should be here as an Adapter is essentially a codified
endpoint_filter.

Add them to the conf options for Adapter, since that is how Adapters get
defined in services which is one of the reasons for doing all of this
work.

Change-Id: I8c6613bac09f28169e903b303c7330b1e90fe72d
---
 keystoneauth1/adapter.py                      | 28 +++++++++-
 keystoneauth1/loading/adapter.py              | 51 +++++++++++++++++++
 .../tests/unit/loading/test_adapter.py        | 38 +++++++++++++-
 3 files changed, 113 insertions(+), 4 deletions(-)

diff --git a/keystoneauth1/adapter.py b/keystoneauth1/adapter.py
index c9981d8d..91ff5a82 100644
--- a/keystoneauth1/adapter.py
+++ b/keystoneauth1/adapter.py
@@ -27,6 +27,9 @@ class Adapter(object):
     particular client that is using the session. An adapter provides a wrapper
     of client local data around the global session object.
 
+    version, min_version and max_version can all be given either as a
+    string or a tuple.
+
     :param session: The session object to wrap.
     :type session: keystoneauth1.session.Session
     :param str service_type: The default service_type for URL discovery.
@@ -35,7 +38,9 @@ class Adapter(object):
     :param str region_name: The default region_name for URL discovery.
     :param str endpoint_override: Always use this endpoint URL for requests
                                   for this client.
-    :param tuple version: The version that this API targets.
+    :param version: The minimum version restricted to a given Major API.
+                    Mutually exclusive with min_version and max_version.
+                    (optional)
     :param auth: An auth plugin to use instead of the session one.
     :type auth: keystoneauth1.plugin.BaseAuthPlugin
     :param str user_agent: The User-Agent string to set.
@@ -64,6 +69,14 @@ class Adapter(object):
                                   ``req-$uuid``) that will be passed on all
                                   requests. Enables cross project request id
                                   tracking.
+    :param min_version: The minimum major version of a given API, intended to
+                        be used as the lower bound of a range with
+                        max_version. Mutually exclusive with version.
+                        If min_version is given with no max_version it is as
+                        if max version is 'latest'. (optional)
+    :param max_version: The maximum major version of a given API, intended to
+                        be used as the upper bound of a range with min_version.
+                        Mutually exclusive with version. (optional)
     """
 
     client_name = None
@@ -76,7 +89,12 @@ class Adapter(object):
                  connect_retries=None, logger=None, allow={},
                  additional_headers=None, client_name=None,
                  client_version=None, allow_version_hack=None,
-                 global_request_id=None):
+                 global_request_id=None,
+                 min_version=None, max_version=None):
+        if version and (min_version or max_version):
+            raise TypeError(
+                "version is mutually exclusive with min_version and"
+                " max_version")
         # NOTE(jamielennox): when adding new parameters to adapter please also
         # add them to the adapter call in httpclient.HTTPClient.__init__ as
         # well as to load_adapter_from_argparse below if the argument is
@@ -96,6 +114,8 @@ class Adapter(object):
         self.allow = allow
         self.additional_headers = additional_headers or {}
         self.allow_version_hack = allow_version_hack
+        self.min_version = min_version
+        self.max_version = max_version
 
         self.global_request_id = global_request_id
 
@@ -115,6 +135,10 @@ class Adapter(object):
             kwargs.setdefault('region_name', self.region_name)
         if self.version:
             kwargs.setdefault('version', self.version)
+        if self.min_version:
+            kwargs.setdefault('min_version', self.min_version)
+        if self.max_version:
+            kwargs.setdefault('max_version', self.max_version)
         if self.allow_version_hack is not None:
             kwargs.setdefault('allow_version_hack', self.allow_version_hack)
 
diff --git a/keystoneauth1/loading/adapter.py b/keystoneauth1/loading/adapter.py
index 5b3638db..256c4282 100644
--- a/keystoneauth1/loading/adapter.py
+++ b/keystoneauth1/loading/adapter.py
@@ -45,6 +45,19 @@ class Adapter(base.BaseLoader):
             :region_name:       The default region_name for URL discovery.
             :endpoint_override: Always use this endpoint URL for requests
                                 for this client.
+            :version:           The minimum version restricted to a given Major
+                                API. Mutually exclusive with min_version and
+                                max_version.
+            :min_version:       The minimum major version of a given API,
+                                intended to be used as the lower bound of a
+                                range with max_version. Mutually exclusive with
+                                version. If min_version is given with no
+                                max_version it is as if max version is
+                                'latest'.
+            :max_version:       The maximum major version of a given API,
+                                intended to be used as the upper bound of a
+                                range with min_version. Mutually exclusive with
+                                version.
 
         :returns: A list of oslo_config options.
         """
@@ -65,6 +78,23 @@ class Adapter(base.BaseLoader):
                 cfg.StrOpt('endpoint-override',
                            help='Always use this endpoint URL for requests '
                                 'for this client.'),
+                cfg.StrOpt('version',
+                           help='Minimum Major API version within a given '
+                                'Major API version for endpoint URL '
+                                'discovery. Mutually exclusive with '
+                                'min_version and max_version'),
+                cfg.StrOpt('min-version',
+                           help='The minimum major version of a given API, '
+                                'intended to be used as the lower bound of a '
+                                'range with max_version. Mutually exclusive '
+                                'with version. If min_version is given with '
+                                'no max_version it is as if max version is '
+                                '"latest".'),
+                cfg.StrOpt('max-version',
+                           help='The maximum major version of a given API, '
+                                'intended to be used as the upper bound of a '
+                                'range with min_version. Mutually exclusive '
+                                'with version.'),
                 ]
 
     def register_conf_options(self, conf, group):
@@ -77,6 +107,19 @@ class Adapter(base.BaseLoader):
             :region_name:       The default region_name for URL discovery.
             :endpoint_override: Always use this endpoint URL for requests
                                 for this client.
+            :version:           The minimum version restricted to a given Major
+                                API. Mutually exclusive with min_version and
+                                max_version.
+            :min_version:       The minimum major version of a given API,
+                                intended to be used as the lower bound of a
+                                range with max_version. Mutually exclusive with
+                                version. If min_version is given with no
+                                max_version it is as if max version is
+                                'latest'.
+            :max_version:       The maximum major version of a given API,
+                                intended to be used as the upper bound of a
+                                range with min_version. Mutually exclusive with
+                                version.
 
         :param oslo_config.Cfg conf: config object to register with.
         :param string group: The ini group to register options in.
@@ -107,6 +150,14 @@ class Adapter(base.BaseLoader):
         kwargs.setdefault('interface', c.interface)
         kwargs.setdefault('region_name', c.region_name)
         kwargs.setdefault('endpoint_override', c.endpoint_override)
+        kwargs.setdefault('version', c.version)
+        kwargs.setdefault('min_version', c.min_version)
+        kwargs.setdefault('max_version', c.max_version)
+        if kwargs['version'] and (
+                kwargs['max_version'] or kwargs['min_version']):
+            raise TypeError(
+                "version is mutually exclusive with min_version and"
+                " max_version")
 
         return self.load_from_options(**kwargs)
 
diff --git a/keystoneauth1/tests/unit/loading/test_adapter.py b/keystoneauth1/tests/unit/loading/test_adapter.py
index f50bca72..aa4bf929 100644
--- a/keystoneauth1/tests/unit/loading/test_adapter.py
+++ b/keystoneauth1/tests/unit/loading/test_adapter.py
@@ -31,7 +31,7 @@ class ConfLoadingTests(utils.TestCase):
         self.conf_fx.config(
             service_type='type', service_name='name', interface='iface',
             region_name='region', endpoint_override='endpoint',
-            group=self.GROUP)
+            version='2.0', group=self.GROUP)
         adap = loading.load_adapter_from_conf_options(
             self.conf_fx.conf, self.GROUP, session='session', auth='auth')
         self.assertEqual('type', adap.service_type)
@@ -41,11 +41,45 @@ class ConfLoadingTests(utils.TestCase):
         self.assertEqual('endpoint', adap.endpoint_override)
         self.assertEqual('session', adap.session)
         self.assertEqual('auth', adap.auth)
+        self.assertEqual('2.0', adap.version)
+        self.assertIsNone(adap.min_version)
+        self.assertIsNone(adap.max_version)
+
+    def test_load_version_range(self):
+        self.conf_fx.config(
+            service_type='type', service_name='name', interface='iface',
+            region_name='region', endpoint_override='endpoint',
+            min_version='2.0', max_version='3.0', group=self.GROUP)
+        adap = loading.load_adapter_from_conf_options(
+            self.conf_fx.conf, self.GROUP, session='session', auth='auth')
+        self.assertEqual('type', adap.service_type)
+        self.assertEqual('name', adap.service_name)
+        self.assertEqual('iface', adap.interface)
+        self.assertEqual('region', adap.region_name)
+        self.assertEqual('endpoint', adap.endpoint_override)
+        self.assertEqual('session', adap.session)
+        self.assertEqual('auth', adap.auth)
+        self.assertIsNone(adap.version)
+        self.assertEqual('2.0', adap.min_version)
+        self.assertEqual('3.0', adap.max_version)
+
+    def test_load_bad_version(self):
+        self.conf_fx.config(
+            service_type='type', service_name='name', interface='iface',
+            region_name='region', endpoint_override='endpoint',
+            version='2.0', min_version='2.0', max_version='3.0',
+            group=self.GROUP)
+
+        self.assertRaises(
+            TypeError,
+            loading.load_adapter_from_conf_options,
+            self.conf_fx.conf, self.GROUP, session='session', auth='auth')
 
     def test_get_conf_options(self):
         opts = loading.get_adapter_conf_options()
         for opt in opts:
             self.assertIsInstance(opt, cfg.StrOpt)
         self.assertEqual({'service-type', 'service-name', 'interface',
-                          'region-name', 'endpoint-override'},
+                          'region-name', 'endpoint-override', 'version',
+                          'min-version', 'max-version'},
                          {opt.name for opt in opts})