Merge "Discover supported APIs"
This commit is contained in:
		| @@ -12,7 +12,30 @@ | ||||
| # License for the specific language governing permissions and limitations | ||||
| # under the License. | ||||
|  | ||||
| from keystoneclient import discover | ||||
| from keystoneclient import httpclient | ||||
|  | ||||
| # Using client.HTTPClient is deprecated. Use httpclient.HTTPClient instead. | ||||
| HTTPClient = httpclient.HTTPClient | ||||
|  | ||||
|  | ||||
| def Client(version=None, unstable=False, **kwargs): | ||||
|     """Factory function to create a new identity service client. | ||||
|  | ||||
|     :param tuple version: The required version of the identity API. If | ||||
|                           specified the client will be selected such that the | ||||
|                           major version is equivalent and an endpoint provides | ||||
|                           at least the specified minor version. For example to | ||||
|                           specify the 3.1 API use (3, 1). | ||||
|     :param bool unstable: Accept endpoints not marked as 'stable'. (optional) | ||||
|     :param kwargs: Additional arguments are passed through to the client | ||||
|                    that is being created. | ||||
|     :returns: New keystone client object | ||||
|               (keystoneclient.v2_0.Client or keystoneclient.v3.Client). | ||||
|  | ||||
|     :raises: DiscoveryFailure if the server's response is invalid | ||||
|     :raises: VersionNotAvailable if a suitable client cannot be found. | ||||
|     """ | ||||
|  | ||||
|     return discover.Discover(**kwargs).create_client(version=version, | ||||
|                                                      unstable=unstable) | ||||
|   | ||||
							
								
								
									
										421
									
								
								keystoneclient/discover.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								keystoneclient/discover.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | ||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||||
|  | ||||
| # 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 logging | ||||
| import six | ||||
|  | ||||
| from keystoneclient import exceptions | ||||
| from keystoneclient import httpclient | ||||
| from keystoneclient.v2_0 import client as v2_client | ||||
| from keystoneclient.v3 import client as v3_client | ||||
|  | ||||
| _logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class _KeystoneVersion(object): | ||||
|     """A factory object that holds all the information to create a client. | ||||
|  | ||||
|      Instances of this class are callable objects that hold all the kwargs that | ||||
|      were passed to discovery so that a user may simply call it to create a new | ||||
|      client object.  Additional arguments passed to the call will override or | ||||
|      add to those provided to the object. | ||||
|     """ | ||||
|  | ||||
|     _CLIENT_VERSIONS = {2: v2_client.Client, | ||||
|                         3: v3_client.Client} | ||||
|  | ||||
|     def __init__(self, version, status, client_class=None, **kwargs): | ||||
|         """Create a new discovered version object. | ||||
|  | ||||
|         :param tuple version: the version of the available API. | ||||
|         :param string status: the stability of the API. | ||||
|         :param Class client_class: the client class that should be used to | ||||
|                                    instantiate against this version of the API. | ||||
|                                    (optional, will be matched against known) | ||||
|         :param dict **kwargs: Additional arguments that should be passed on to | ||||
|                               the client when it is constructed. | ||||
|         """ | ||||
|         self.version = version | ||||
|         self.status = status | ||||
|         self.client_class = client_class | ||||
|         self.client_kwargs = kwargs | ||||
|  | ||||
|         if not self.client_class: | ||||
|             try: | ||||
|                 self.client_class = self._CLIENT_VERSIONS[self.version[0]] | ||||
|             except KeyError: | ||||
|                 raise exceptions.DiscoveryFailure("No client available " | ||||
|                                                   "for version: %s" % | ||||
|                                                   self.version) | ||||
|  | ||||
|     def __lt__(self, other): | ||||
|         """Version Ordering. | ||||
|  | ||||
|         Versions are ordered by major, then minor version number, then | ||||
|         'stable' is deemed the highest possible status, then they are just | ||||
|         treated alphabetically (alpha < beta etc) | ||||
|         """ | ||||
|         if self.version == other.version: | ||||
|             if self.status == 'stable': | ||||
|                 return False | ||||
|             elif other.status == 'stable': | ||||
|                 return True | ||||
|             else: | ||||
|                 return self.status < other.status | ||||
|  | ||||
|         return self.version < other.version | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return self.version == other.version and self.status == other.status | ||||
|  | ||||
|     def __call__(self, **kwargs): | ||||
|         if kwargs: | ||||
|             client_kwargs = self.client_kwargs.copy() | ||||
|             client_kwargs.update(kwargs) | ||||
|         else: | ||||
|             client_kwargs = self.client_kwargs | ||||
|         return self.client_class(**client_kwargs) | ||||
|  | ||||
|     @property | ||||
|     def _str_ver(self): | ||||
|         ver = ".".join([str(v) for v in self.version]) | ||||
|  | ||||
|         if self.status != 'stable': | ||||
|             ver = "%s-%s" % (ver, self.status) | ||||
|  | ||||
|         return ver | ||||
|  | ||||
|  | ||||
| def _normalize_version_number(version): | ||||
|     """Turn a version representation into a tuple.""" | ||||
|  | ||||
|     # trim the v from a 'v2.0' or similar | ||||
|     try: | ||||
|         version = version.lstrip('v') | ||||
|     except AttributeError: | ||||
|         pass | ||||
|  | ||||
|     # if it's an integer or a numeric as a string then normalize it | ||||
|     # to a string, this ensures 1 decimal point | ||||
|     try: | ||||
|         num = float(version) | ||||
|     except Exception: | ||||
|         pass | ||||
|     else: | ||||
|         version = str(num) | ||||
|  | ||||
|     # if it's a string (or an integer) from above break it on . | ||||
|     try: | ||||
|         return tuple(map(int, version.split("."))) | ||||
|     except Exception: | ||||
|         pass | ||||
|  | ||||
|     # last attempt, maybe it's a list or iterable. | ||||
|     try: | ||||
|         return tuple(map(int, version)) | ||||
|     except Exception: | ||||
|         pass | ||||
|  | ||||
|     raise TypeError("Invalid version specified: %s" % version) | ||||
|  | ||||
|  | ||||
| def available_versions(url, **kwargs): | ||||
|     headers = {'Accept': 'application/json'} | ||||
|  | ||||
|     client = httpclient.HTTPClient(**kwargs) | ||||
|     resp, body_resp = client.request(url, 'GET', headers=headers) | ||||
|  | ||||
|     # In the event of querying a root URL we will get back a list of | ||||
|     # available versions. | ||||
|     try: | ||||
|         return body_resp['versions']['values'] | ||||
|     except (KeyError, TypeError): | ||||
|         pass | ||||
|  | ||||
|     # Otherwise if we query an endpoint like /v2.0 then we will get back | ||||
|     # just the one available version. | ||||
|     try: | ||||
|         return [body_resp['version']] | ||||
|     except (KeyError, TypeError): | ||||
|         pass | ||||
|  | ||||
|     raise exceptions.DiscoveryFailure("Invalid Response - Bad version" | ||||
|                                       " data returned: %s" % body_resp) | ||||
|  | ||||
|  | ||||
| class Discover(object): | ||||
|     """A means to discover and create clients depending on the supported API | ||||
|     versions on the server. | ||||
|  | ||||
|     Querying the server is done on object creation and every subsequent method | ||||
|     operates upon the data that was retrieved. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         """Construct a new discovery object. | ||||
|  | ||||
|         The connection parameters associated with this method are the same | ||||
|         format and name as those used by a client (see | ||||
|         keystoneclient.v2_0.client.Client and keystoneclient.v3.client.Client). | ||||
|         If not overridden in subsequent methods they will also be what is | ||||
|         passed to the constructed client. | ||||
|  | ||||
|         In the event that auth_url and endpoint is provided then auth_url will | ||||
|         be used in accordance with how the client operates. | ||||
|  | ||||
|         The initialization process also queries the server. | ||||
|  | ||||
|         :param string auth_url: Identity service endpoint for authorization. | ||||
|                                 (optional) | ||||
|         :param string endpoint: A user-supplied endpoint URL for the identity | ||||
|                                 service. (optional) | ||||
|         :param string original_ip: The original IP of the requesting user | ||||
|                                    which will be sent to identity service in a | ||||
|                                    'Forwarded' header. (optional) | ||||
|         :param boolean debug: Enables debug logging of all request and | ||||
|                               responses to the identity service. | ||||
|                               default False (optional) | ||||
|         :param string cacert: Path to the Privacy Enhanced Mail (PEM) file | ||||
|                               which contains the trusted authority X.509 | ||||
|                               certificates needed to established SSL connection | ||||
|                               with the identity service. (optional) | ||||
|         :param string key: Path to the Privacy Enhanced Mail (PEM) file which | ||||
|                            contains the unencrypted client private key needed | ||||
|                            to established two-way SSL connection with the | ||||
|                            identity service. (optional) | ||||
|         :param string cert: Path to the Privacy Enhanced Mail (PEM) file which | ||||
|                             contains the corresponding X.509 client certificate | ||||
|                             needed to established two-way SSL connection with | ||||
|                             the identity service. (optional) | ||||
|         :param boolean insecure: Does not perform X.509 certificate validation | ||||
|                                  when establishing SSL connection with identity | ||||
|                                  service. default: False (optional) | ||||
|         """ | ||||
|  | ||||
|         url = kwargs.get('endpoint') or kwargs.get('auth_url') | ||||
|         if not url: | ||||
|             raise exceptions.DiscoveryFailure('Not enough information to ' | ||||
|                                               'determine URL. Provide either ' | ||||
|                                               'auth_url or endpoint') | ||||
|  | ||||
|         self._client_kwargs = kwargs | ||||
|         self._available_versions = available_versions(url, **kwargs) | ||||
|  | ||||
|     def _get_client_constructor_kwargs(self, kwargs_dict={}, **kwargs): | ||||
|         client_kwargs = self._client_kwargs.copy() | ||||
|  | ||||
|         client_kwargs.update(kwargs_dict) | ||||
|         client_kwargs.update(**kwargs) | ||||
|  | ||||
|         return client_kwargs | ||||
|  | ||||
|     def available_versions(self, unstable=False): | ||||
|         """Return a list of identity APIs available on the server and the data | ||||
|         associated with them. | ||||
|  | ||||
|         :param bool unstable: Accept endpoints not marked 'stable'. (optional) | ||||
|  | ||||
|         :returns: A List of dictionaries as presented by the server. Each dict | ||||
|                   will contain the version and the URL to use for the version. | ||||
|                   It is a direct representation of the layout presented by the | ||||
|                   identity API. | ||||
|  | ||||
|         Example:: | ||||
|  | ||||
|             >>> from keystoneclient import discover | ||||
|             >>> disc = discover.Discovery(auth_url='http://localhost:5000') | ||||
|             >>> disc.available_versions() | ||||
|                 [{'id': 'v3.0', | ||||
|                     'links': [{'href': u'http://127.0.0.1:5000/v3/', | ||||
|                                'rel': u'self'}], | ||||
|                   'media-types': [ | ||||
|                       {'base': 'application/json', | ||||
|                        'type': 'application/vnd.openstack.identity-v3+json'}, | ||||
|                       {'base': 'application/xml', | ||||
|                        'type': 'application/vnd.openstack.identity-v3+xml'}], | ||||
|                   'status': 'stable', | ||||
|                   'updated': '2013-03-06T00:00:00Z'}, | ||||
|                  {'id': 'v2.0', | ||||
|                   'links': [{'href': u'http://127.0.0.1:5000/v2.0/', | ||||
|                              'rel': u'self'}, | ||||
|                             {'href': u'...', | ||||
|                              'rel': u'describedby', | ||||
|                              'type': u'application/pdf'}], | ||||
|                   'media-types': [ | ||||
|                       {'base': 'application/json', | ||||
|                        'type': 'application/vnd.openstack.identity-v2.0+json'}, | ||||
|                       {'base': 'application/xml', | ||||
|                        'type': 'application/vnd.openstack.identity-v2.0+xml'}], | ||||
|                   'status': 'stable', | ||||
|                   'updated': '2013-03-06T00:00:00Z'}] | ||||
|         """ | ||||
|         if unstable: | ||||
|             # no need to determine the stable endpoints, just return everything | ||||
|             return self._available_versions | ||||
|  | ||||
|         versions = [] | ||||
|  | ||||
|         for v in self._available_versions: | ||||
|             try: | ||||
|                 status = v['status'] | ||||
|             except KeyError: | ||||
|                 _logger.warning("Skipping over invalid version data. " | ||||
|                                 "No stability status in version.") | ||||
|             else: | ||||
|                 if status == 'stable': | ||||
|                     versions.append(v) | ||||
|  | ||||
|         return versions | ||||
|  | ||||
|     def _get_factory_from_response_entry(self, version_data, **kwargs): | ||||
|         """Create a _KeystoneVersion factory object from a version response | ||||
|         entry returned from a server. | ||||
|         """ | ||||
|         try: | ||||
|             version_str = version_data['id'] | ||||
|             status = version_data['status'] | ||||
|  | ||||
|             if not version_str.startswith('v'): | ||||
|                 raise exceptions.DiscoveryFailure('Skipping over invalid ' | ||||
|                                                   'version string: %s. It ' | ||||
|                                                   'should start with a v.' % | ||||
|                                                   version_str) | ||||
|  | ||||
|             for link in version_data['links']: | ||||
|                 # NOTE(jamielennox): there are plenty of links like with | ||||
|                 # documentation and such, we only care about the self | ||||
|                 # which is a link to the URL we should use. | ||||
|                 if link['rel'].lower() == 'self': | ||||
|                     version_number = _normalize_version_number(version_str) | ||||
|                     version_url = link['href'] | ||||
|                     break | ||||
|             else: | ||||
|                 raise exceptions.DiscoveryFailure("Didn't find any links " | ||||
|                                                   "in version data.") | ||||
|  | ||||
|         except (KeyError, TypeError, ValueError): | ||||
|             raise exceptions.DiscoveryFailure('Skipping over invalid ' | ||||
|                                               'version data.') | ||||
|  | ||||
|         # NOTE(jamielennox): the url might be the auth_url or the endpoint | ||||
|         # depending on what was passed initially. Order is important, endpoint | ||||
|         # needs to override auth_url. | ||||
|         for url_type in ('auth_url', 'endpoint'): | ||||
|             if self._client_kwargs.get(url_type, False): | ||||
|                 kwargs[url_type] = version_url | ||||
|             else: | ||||
|                 kwargs[url_type] = None | ||||
|  | ||||
|         return _KeystoneVersion(status=status, | ||||
|                                 version=version_number, | ||||
|                                 **kwargs) | ||||
|  | ||||
|     def _available_clients(self, unstable=False, **kwargs): | ||||
|         """Return a dictionary of factory functions for available API versions. | ||||
|  | ||||
|         :returns: A dictionary of available API endpoints with the version | ||||
|                   number as a tuple as the key, and a factory object that can | ||||
|                   be used to create an appropriate client as the value. | ||||
|  | ||||
|         To use the returned factory you simply call it with the parameters to | ||||
|         pass to keystoneclient. These parameters will override those saved in | ||||
|         the factory. | ||||
|  | ||||
|         Example:: | ||||
|  | ||||
|             >>> from keystoneclient import client | ||||
|             >>> available_clients = client._available_clients(auth_url=url, | ||||
|             ...                                               **kwargs) | ||||
|             >>> try: | ||||
|             ...     v2_factory = available_clients[(2, 0)] | ||||
|             ... except KeyError: | ||||
|             ...     print "Version 2.0 unavailable" | ||||
|             ... else: | ||||
|             ...     v2_client = v2_factory(token='abcdef') | ||||
|             ...     v2_client.tenants.list() | ||||
|  | ||||
|         :raises: DiscoveryFailure if the response is invalid | ||||
|         :raises: VersionNotAvailable if a suitable client cannot be found. | ||||
|         """ | ||||
|  | ||||
|         versions = dict() | ||||
|         response_values = self.available_versions(unstable=unstable) | ||||
|         client_kwargs = self._get_client_constructor_kwargs(kwargs_dict=kwargs) | ||||
|  | ||||
|         for version_data in response_values: | ||||
|             try: | ||||
|                 v = self._get_factory_from_response_entry(version_data, | ||||
|                                                           **client_kwargs) | ||||
|             except exceptions.DiscoveryFailure, e: | ||||
|                 _logger.warning("Invalid entry: %s", e, exc_info=True) | ||||
|             else: | ||||
|                 versions[v.version] = v | ||||
|  | ||||
|         return versions | ||||
|  | ||||
|     def create_client(self, version=None, **kwargs): | ||||
|         """Factory function to create a new identity service client. | ||||
|  | ||||
|         :param tuple version: The required version of the identity API. If | ||||
|                               specified the client will be selected such that | ||||
|                               the major version is equivalent and an endpoint | ||||
|                               provides at least the specified minor version. | ||||
|                               For example to specify the 3.1 API use (3, 1). | ||||
|                               (optional) | ||||
|         :param bool unstable: Accept endpoints not marked 'stable'. (optional) | ||||
|         :param kwargs: Additional arguments will override those provided to | ||||
|                        this object's constructor. | ||||
|  | ||||
|         :returns: An instantiated identity client object. | ||||
|  | ||||
|         :raises: DiscoveryFailure if the server response is invalid | ||||
|         :raises: VersionNotAvailable if a suitable client cannot be found. | ||||
|         """ | ||||
|         versions = self._available_clients(**kwargs) | ||||
|         chosen = None | ||||
|  | ||||
|         if version: | ||||
|             version = _normalize_version_number(version) | ||||
|  | ||||
|             for keystone_version in six.itervalues(versions): | ||||
|                 # major versions must be the same (eg even though v2 is a lower | ||||
|                 # version than v3 we can't use it if v2 was requested) | ||||
|                 if version[0] != keystone_version.version[0]: | ||||
|                     continue | ||||
|  | ||||
|                 # prevent selecting a minor version less than what is required | ||||
|                 if version <= keystone_version.version: | ||||
|                     chosen = keystone_version | ||||
|                     break | ||||
|  | ||||
|         elif versions: | ||||
|             # if no version specified pick the latest one | ||||
|             chosen = max(six.iteritems(versions))[1] | ||||
|  | ||||
|         if not chosen: | ||||
|             msg = "Could not find a suitable endpoint" | ||||
|  | ||||
|             if version: | ||||
|                 msg = "%s for client version: %s" % (msg, version) | ||||
|  | ||||
|             if versions: | ||||
|                 available = ", ".join([v._str_ver | ||||
|                                        for v in six.itervalues(versions)]) | ||||
|                 msg = "%s. Available_versions are: %s" % (msg, available) | ||||
|             else: | ||||
|                 msg = "%s. No versions reported available" % msg | ||||
|  | ||||
|             raise exceptions.VersionNotAvailable(msg) | ||||
|  | ||||
|         return chosen() | ||||
| @@ -41,3 +41,11 @@ class SSLError(ConnectionError): | ||||
|  | ||||
| class Timeout(ClientException): | ||||
|     """The request timed out.""" | ||||
|  | ||||
|  | ||||
| class DiscoveryFailure(ClientException): | ||||
|     """Discovery of client versions failed.""" | ||||
|  | ||||
|  | ||||
| class VersionNotAvailable(DiscoveryFailure): | ||||
|     """Discovery failed as the version you requested is not available.""" | ||||
|   | ||||
							
								
								
									
										459
									
								
								keystoneclient/tests/test_discovery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										459
									
								
								keystoneclient/tests/test_discovery.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,459 @@ | ||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 | ||||
|  | ||||
| # 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 httpretty | ||||
| from testtools import matchers | ||||
|  | ||||
| from keystoneclient import client | ||||
| from keystoneclient import discover | ||||
| from keystoneclient import exceptions | ||||
| from keystoneclient.openstack.common import jsonutils | ||||
| from keystoneclient.tests import utils | ||||
| from keystoneclient.v2_0 import client as v2_client | ||||
| from keystoneclient.v3 import client as v3_client | ||||
|  | ||||
| BASE_HOST = 'http://keystone.example.com' | ||||
| BASE_URL = "%s:5000/" % BASE_HOST | ||||
| UPDATED = '2013-03-06T00:00:00Z' | ||||
|  | ||||
| TEST_SERVICE_CATALOG = [{ | ||||
|     "endpoints": [{ | ||||
|         "adminURL": "%s:8774/v1.0" % BASE_HOST, | ||||
|         "region": "RegionOne", | ||||
|         "internalURL": "%s://127.0.0.1:8774/v1.0" % BASE_HOST, | ||||
|         "publicURL": "%s:8774/v1.0/" % BASE_HOST | ||||
|     }], | ||||
|     "type": "nova_compat", | ||||
|     "name": "nova_compat" | ||||
| }, { | ||||
|     "endpoints": [{ | ||||
|         "adminURL": "http://nova/novapi/admin", | ||||
|         "region": "RegionOne", | ||||
|         "internalURL": "http://nova/novapi/internal", | ||||
|         "publicURL": "http://nova/novapi/public" | ||||
|     }], | ||||
|     "type": "compute", | ||||
|     "name": "nova" | ||||
| }, { | ||||
|     "endpoints": [{ | ||||
|         "adminURL": "http://glance/glanceapi/admin", | ||||
|         "region": "RegionOne", | ||||
|         "internalURL": "http://glance/glanceapi/internal", | ||||
|         "publicURL": "http://glance/glanceapi/public" | ||||
|     }], | ||||
|     "type": "image", | ||||
|     "name": "glance" | ||||
| }, { | ||||
|     "endpoints": [{ | ||||
|         "adminURL": "%s:35357/v2.0" % BASE_HOST, | ||||
|         "region": "RegionOne", | ||||
|         "internalURL": "%s:5000/v2.0" % BASE_HOST, | ||||
|         "publicURL": "%s:5000/v2.0" % BASE_HOST | ||||
|     }], | ||||
|     "type": "identity", | ||||
|     "name": "keystone" | ||||
| }, { | ||||
|     "endpoints": [{ | ||||
|         "adminURL": "http://swift/swiftapi/admin", | ||||
|         "region": "RegionOne", | ||||
|         "internalURL": "http://swift/swiftapi/internal", | ||||
|         "publicURL": "http://swift/swiftapi/public" | ||||
|     }], | ||||
|     "type": "object-store", | ||||
|     "name": "swift" | ||||
| }] | ||||
|  | ||||
| V2_URL = "%sv2.0" % BASE_URL | ||||
| V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' | ||||
|                                 'openstack-identity-service/2.0/content/', | ||||
|                         'rel': 'describedby', | ||||
|                         'type': 'text/html'} | ||||
| V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' | ||||
|                                'ity-service/2.0/identity-dev-guide-2.0.pdf', | ||||
|                        'rel': 'describedby', | ||||
|                        'type': 'application/pdf'} | ||||
|  | ||||
| V2_VERSION = {'id': 'v2.0', | ||||
|               'links': [{'href': V2_URL, 'rel': 'self'}, | ||||
|                         V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], | ||||
|               'status': 'stable', | ||||
|               'updated': UPDATED} | ||||
|  | ||||
| V2_AUTH_RESPONSE = jsonutils.dumps({ | ||||
|     "access": { | ||||
|         "token": { | ||||
|             "expires": "2020-01-01T00:00:10.000123Z", | ||||
|             "id": 'fakeToken', | ||||
|             "tenant": { | ||||
|                 "id": '1' | ||||
|             }, | ||||
|         }, | ||||
|         "user": { | ||||
|             "id": 'test' | ||||
|         }, | ||||
|         "serviceCatalog": TEST_SERVICE_CATALOG, | ||||
|     }, | ||||
| }) | ||||
|  | ||||
| V3_URL = "%sv3" % BASE_URL | ||||
| V3_MEDIA_TYPES = [{'base': 'application/json', | ||||
|                    'type': 'application/vnd.openstack.identity-v3+json'}, | ||||
|                   {'base': 'application/xml', | ||||
|                    'type': 'application/vnd.openstack.identity-v3+xml'}] | ||||
|  | ||||
| V3_VERSION = {'id': 'v3.0', | ||||
|               'links': [{'href': V3_URL, 'rel': 'self'}], | ||||
|               'media-types': V3_MEDIA_TYPES, | ||||
|               'status': 'stable', | ||||
|               'updated': UPDATED} | ||||
|  | ||||
| V3_TOKEN = u'3e2813b7ba0b4006840c3825860b86ed', | ||||
| V3_AUTH_RESPONSE = jsonutils.dumps({ | ||||
|     "token": { | ||||
|         "methods": [ | ||||
|             "token", | ||||
|             "password" | ||||
|         ], | ||||
|  | ||||
|         "expires_at": "2020-01-01T00:00:10.000123Z", | ||||
|         "project": { | ||||
|             "domain": { | ||||
|                 "id": '1', | ||||
|                 "name": 'test-domain' | ||||
|             }, | ||||
|             "id": '1', | ||||
|             "name": 'test-project' | ||||
|         }, | ||||
|         "user": { | ||||
|             "domain": { | ||||
|                 "id": '1', | ||||
|                 "name": 'test-domain' | ||||
|             }, | ||||
|             "id": '1', | ||||
|             "name": 'test-user' | ||||
|         }, | ||||
|         "issued_at": "2013-05-29T16:55:21.468960Z", | ||||
|     }, | ||||
| }) | ||||
|  | ||||
|  | ||||
| def _create_version_list(versions): | ||||
|     return jsonutils.dumps({'versions': {'values': versions}}) | ||||
|  | ||||
|  | ||||
| def _create_single_version(version): | ||||
|     return jsonutils.dumps({'version': version}) | ||||
|  | ||||
|  | ||||
| V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) | ||||
| V2_VERSION_LIST = _create_version_list([V2_VERSION]) | ||||
|  | ||||
| V3_VERSION_ENTRY = _create_single_version(V3_VERSION) | ||||
| V2_VERSION_ENTRY = _create_single_version(V2_VERSION) | ||||
|  | ||||
|  | ||||
| @httpretty.activate | ||||
| class AvailableVersionsTests(utils.TestCase): | ||||
|  | ||||
|     def test_available_versions(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         versions = discover.available_versions(BASE_URL) | ||||
|  | ||||
|         for v in versions: | ||||
|             self.assertIn('id', v) | ||||
|             self.assertIn('status', v) | ||||
|             self.assertIn('links', v) | ||||
|  | ||||
|     def test_available_versions_individual(self): | ||||
|         httpretty.register_uri(httpretty.GET, V3_URL, status=200, | ||||
|                                body=V3_VERSION_ENTRY) | ||||
|  | ||||
|         versions = discover.available_versions(V3_URL) | ||||
|  | ||||
|         for v in versions: | ||||
|             self.assertEqual(v['id'], 'v3.0') | ||||
|             self.assertEqual(v['status'], 'stable') | ||||
|             self.assertIn('media-types', v) | ||||
|             self.assertIn('links', v) | ||||
|  | ||||
|  | ||||
| @httpretty.activate | ||||
| class ClientDiscoveryTests(utils.TestCase): | ||||
|  | ||||
|     def assertCreatesV3(self, **kwargs): | ||||
|         httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL, | ||||
|                                body=V3_AUTH_RESPONSE, X_Subject_Token=V3_TOKEN) | ||||
|  | ||||
|         kwargs.setdefault('username', 'foo') | ||||
|         kwargs.setdefault('password', 'bar') | ||||
|         keystone = client.Client(**kwargs) | ||||
|         self.assertIsInstance(keystone, v3_client.Client) | ||||
|         return keystone | ||||
|  | ||||
|     def assertCreatesV2(self, **kwargs): | ||||
|         httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL, | ||||
|                                body=V2_AUTH_RESPONSE) | ||||
|  | ||||
|         kwargs.setdefault('username', 'foo') | ||||
|         kwargs.setdefault('password', 'bar') | ||||
|         keystone = client.Client(**kwargs) | ||||
|         self.assertIsInstance(keystone, v2_client.Client) | ||||
|         return keystone | ||||
|  | ||||
|     def assertVersionNotAvailable(self, **kwargs): | ||||
|         kwargs.setdefault('username', 'foo') | ||||
|         kwargs.setdefault('password', 'bar') | ||||
|  | ||||
|         self.assertRaises(exceptions.VersionNotAvailable, | ||||
|                           client.Client, **kwargs) | ||||
|  | ||||
|     def assertDiscoveryFailure(self, **kwargs): | ||||
|         kwargs.setdefault('username', 'foo') | ||||
|         kwargs.setdefault('password', 'bar') | ||||
|  | ||||
|         self.assertRaises(exceptions.DiscoveryFailure, | ||||
|                           client.Client, **kwargs) | ||||
|  | ||||
|     def test_discover_v3(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         self.assertCreatesV3(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_discover_v2(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V2_VERSION_LIST) | ||||
|         httpretty.register_uri(httpretty.POST, "%s/tokens" % V2_URL, | ||||
|                                body=V2_AUTH_RESPONSE) | ||||
|  | ||||
|         self.assertCreatesV2(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_discover_endpoint_v2(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V2_VERSION_LIST) | ||||
|         self.assertCreatesV2(endpoint=BASE_URL, token='fake-token') | ||||
|  | ||||
|     def test_discover_endpoint_v3(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|         self.assertCreatesV3(endpoint=BASE_URL, token='fake-token') | ||||
|  | ||||
|     def test_discover_invalid_major_version(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         self.assertVersionNotAvailable(auth_url=BASE_URL, version=5) | ||||
|  | ||||
|     def test_discover_200_response_fails(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=200, body='ok') | ||||
|         self.assertDiscoveryFailure(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_discover_minor_greater_than_available_fails(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         self.assertVersionNotAvailable(endpoint=BASE_URL, version=3.4) | ||||
|  | ||||
|     def test_discover_individual_version_v2(self): | ||||
|         httpretty.register_uri(httpretty.GET, V2_URL, status=200, | ||||
|                                body=V2_VERSION_ENTRY) | ||||
|  | ||||
|         self.assertCreatesV2(auth_url=V2_URL) | ||||
|  | ||||
|     def test_discover_individual_version_v3(self): | ||||
|         httpretty.register_uri(httpretty.GET, V3_URL, status=200, | ||||
|                                body=V3_VERSION_ENTRY) | ||||
|  | ||||
|         self.assertCreatesV3(auth_url=V3_URL) | ||||
|  | ||||
|     def test_discover_individual_endpoint_v2(self): | ||||
|         httpretty.register_uri(httpretty.GET, V2_URL, status=200, | ||||
|                                body=V2_VERSION_ENTRY) | ||||
|         self.assertCreatesV2(endpoint=V2_URL, token='fake-token') | ||||
|  | ||||
|     def test_discover_individual_endpoint_v3(self): | ||||
|         httpretty.register_uri(httpretty.GET, V3_URL, status=200, | ||||
|                                body=V3_VERSION_ENTRY) | ||||
|         self.assertCreatesV3(endpoint=V3_URL, token='fake-token') | ||||
|  | ||||
|     def test_discover_fail_to_create_bad_individual_version(self): | ||||
|         httpretty.register_uri(httpretty.GET, V2_URL, status=200, | ||||
|                                body=V2_VERSION_ENTRY) | ||||
|         httpretty.register_uri(httpretty.GET, V3_URL, status=200, | ||||
|                                body=V3_VERSION_ENTRY) | ||||
|  | ||||
|         self.assertVersionNotAvailable(auth_url=V2_URL, version=3) | ||||
|         self.assertVersionNotAvailable(auth_url=V3_URL, version=2) | ||||
|  | ||||
|     def test_discover_unstable_versions(self): | ||||
|         v3_unstable_version = V3_VERSION.copy() | ||||
|         v3_unstable_version['status'] = 'beta' | ||||
|         version_list = _create_version_list([v3_unstable_version, V2_VERSION]) | ||||
|  | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=version_list) | ||||
|  | ||||
|         self.assertCreatesV2(auth_url=BASE_URL) | ||||
|         self.assertVersionNotAvailable(auth_url=BASE_URL, version=3) | ||||
|         self.assertCreatesV3(auth_url=BASE_URL, unstable=True) | ||||
|  | ||||
|     def test_discover_forwards_original_ip(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         ip = '192.168.1.1' | ||||
|         self.assertCreatesV3(auth_url=BASE_URL, original_ip=ip) | ||||
|  | ||||
|         self.assertThat(httpretty.httpretty.last_request.headers['forwarded'], | ||||
|                         matchers.Contains(ip)) | ||||
|  | ||||
|     def test_discover_bad_args(self): | ||||
|         self.assertRaises(exceptions.DiscoveryFailure, | ||||
|                           client.Client) | ||||
|  | ||||
|     def test_discover_bad_response(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=jsonutils.dumps({'FOO': 'BAR'})) | ||||
|         self.assertDiscoveryFailure(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_discovery_ignore_invalid(self): | ||||
|         resp = [{'id': '3.99',  # without a leading v | ||||
|                  'links': [{'href': V3_URL, 'rel': 'self'}], | ||||
|                  'media-types': V3_MEDIA_TYPES, | ||||
|                  'status': 'stable', | ||||
|                  'updated': UPDATED}, | ||||
|                 {'id': 'v3.0', | ||||
|                  'links': [1, 2, 3, 4],  # invalid links | ||||
|                  'media-types': V3_MEDIA_TYPES, | ||||
|                  'status': 'stable', | ||||
|                  'updated': UPDATED}] | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=_create_version_list(resp)) | ||||
|         self.assertDiscoveryFailure(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_ignore_entry_without_links(self): | ||||
|         v3 = V3_VERSION.copy() | ||||
|         v3['links'] = [] | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=_create_version_list([v3, V2_VERSION])) | ||||
|         self.assertCreatesV2(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_ignore_entry_without_status(self): | ||||
|         v3 = V3_VERSION.copy() | ||||
|         del v3['status'] | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=_create_version_list([v3, V2_VERSION])) | ||||
|         self.assertCreatesV2(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_greater_version_than_required(self): | ||||
|         resp = [{'id': 'v3.6', | ||||
|                  'links': [{'href': V3_URL, 'rel': 'self'}], | ||||
|                  'media-types': V3_MEDIA_TYPES, | ||||
|                  'status': 'stable', | ||||
|                  'updated': UPDATED}] | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=200, | ||||
|                                body=_create_version_list(resp)) | ||||
|         self.assertCreatesV3(auth_url=BASE_URL, version=(3, 4)) | ||||
|  | ||||
|     def test_lesser_version_than_required(self): | ||||
|         resp = [{'id': 'v3.4', | ||||
|                  'links': [{'href': V3_URL, 'rel': 'self'}], | ||||
|                  'media-types': V3_MEDIA_TYPES, | ||||
|                  'status': 'stable', | ||||
|                  'updated': UPDATED}] | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=200, | ||||
|                                body=_create_version_list(resp)) | ||||
|         self.assertVersionNotAvailable(auth_url=BASE_URL, version=(3, 6)) | ||||
|  | ||||
|     def test_bad_response(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body="Ugly Duckling") | ||||
|         self.assertDiscoveryFailure(auth_url=BASE_URL) | ||||
|  | ||||
|     def test_pass_client_arguments(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V2_VERSION_LIST) | ||||
|         kwargs = {'original_ip': '100', 'use_keyring': False, | ||||
|                   'stale_duration': 15} | ||||
|  | ||||
|         cl = self.assertCreatesV2(auth_url=BASE_URL, **kwargs) | ||||
|  | ||||
|         self.assertEqual(cl.original_ip, '100') | ||||
|         self.assertEqual(cl.stale_duration, 15) | ||||
|         self.assertFalse(cl.use_keyring) | ||||
|  | ||||
|     def test_overriding_stored_kwargs(self): | ||||
|         httpretty.register_uri(httpretty.GET, BASE_URL, status=300, | ||||
|                                body=V3_VERSION_LIST) | ||||
|  | ||||
|         httpretty.register_uri(httpretty.POST, "%s/auth/tokens" % V3_URL, | ||||
|                                body=V3_AUTH_RESPONSE, X_Subject_Token=V3_TOKEN) | ||||
|  | ||||
|         disc = discover.Discover(auth_url=BASE_URL, debug=False, | ||||
|                                  username='foo') | ||||
|         client = disc.create_client(debug=True, password='bar') | ||||
|  | ||||
|         self.assertIsInstance(client, v3_client.Client) | ||||
|         self.assertTrue(client.debug_log) | ||||
|         self.assertFalse(disc._client_kwargs['debug']) | ||||
|         self.assertEqual(client.username, 'foo') | ||||
|         self.assertEqual(client.password, 'bar') | ||||
|  | ||||
|  | ||||
| class DiscoverUtils(utils.TestCase): | ||||
|  | ||||
|     def test_version_number(self): | ||||
|         def assertVersion(inp, out): | ||||
|             self.assertEqual(discover._normalize_version_number(inp), out) | ||||
|  | ||||
|         def versionRaises(inp): | ||||
|             self.assertRaises(TypeError, | ||||
|                               discover._normalize_version_number, | ||||
|                               inp) | ||||
|  | ||||
|         assertVersion('v1.2', (1, 2)) | ||||
|         assertVersion('v11', (11, 0)) | ||||
|         assertVersion('1.2', (1, 2)) | ||||
|         assertVersion('1.5.1', (1, 5, 1)) | ||||
|         assertVersion('1', (1, 0)) | ||||
|         assertVersion(1, (1, 0)) | ||||
|         assertVersion(5.2, (5, 2)) | ||||
|         assertVersion((6, 1), (6, 1)) | ||||
|         assertVersion([1, 4], (1, 4)) | ||||
|  | ||||
|         versionRaises('hello') | ||||
|         versionRaises('1.a') | ||||
|         versionRaises('vaccuum') | ||||
|  | ||||
|     def test_keystone_version_objects(self): | ||||
|         v31s = discover._KeystoneVersion((3, 1), 'stable') | ||||
|         v20s = discover._KeystoneVersion((2, 0), 'stable') | ||||
|         v30s = discover._KeystoneVersion((3, 0), 'stable') | ||||
|  | ||||
|         v31a = discover._KeystoneVersion((3, 1), 'alpha') | ||||
|         v31b = discover._KeystoneVersion((3, 1), 'beta') | ||||
|  | ||||
|         self.assertTrue(v31s > v30s) | ||||
|         self.assertTrue(v30s > v20s) | ||||
|  | ||||
|         self.assertTrue(v31s > v31a) | ||||
|         self.assertFalse(v31s < v31a) | ||||
|         self.assertTrue(v31b > v31a) | ||||
|         self.assertTrue(v31a < v31b) | ||||
|         self.assertTrue(v31b > v30s) | ||||
|  | ||||
|         self.assertNotEqual(v31s, v31b) | ||||
|         self.assertEqual(v31s, discover._KeystoneVersion((3, 1), 'stable')) | ||||
| @@ -121,10 +121,11 @@ class Client(httpclient.HTTPClient): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     version = 'v2.0' | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         """Initialize a new client for the Keystone v2.0 API.""" | ||||
|         super(Client, self).__init__(**kwargs) | ||||
|         self.version = 'v2.0' | ||||
|         self.endpoints = endpoints.EndpointManager(self) | ||||
|         self.roles = roles.RoleManager(self) | ||||
|         self.services = services.ServiceManager(self) | ||||
|   | ||||
| @@ -84,11 +84,12 @@ class Client(httpclient.HTTPClient): | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     version = 'v3' | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         """Initialize a new client for the Keystone v3 API.""" | ||||
|         super(Client, self).__init__(**kwargs) | ||||
|  | ||||
|         self.version = 'v3' | ||||
|         self.credentials = credentials.CredentialManager(self) | ||||
|         self.endpoints = endpoints.EndpointManager(self) | ||||
|         self.domains = domains.DomainManager(self) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jenkins
					Jenkins