Reorganize exceptions
I hope this to be the base exceptions for all clients. Reorganize them to make sense, remove some we don't use and make use of them. Change-Id: I1e0396d86b787235b3aa34818436709e55446ba7
This commit is contained in:
		| @@ -235,7 +235,7 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): | ||||
|         try: | ||||
|             disc = self.get_discovery(session, hacked_url, authenticated=False) | ||||
|         except (exceptions.DiscoveryFailure, | ||||
|                 exceptions.HTTPError, | ||||
|                 exceptions.HttpError, | ||||
|                 exceptions.ConnectionError): | ||||
|             # NOTE(jamielennox): Again if we can't contact the server we fall | ||||
|             # back to just returning the URL from the catalog. This may not be | ||||
|   | ||||
| @@ -128,7 +128,7 @@ class BaseGenericPlugin(base.BaseIdentityPlugin): | ||||
|                                       self.auth_url, | ||||
|                                       authenticated=False) | ||||
|         except (exceptions.DiscoveryFailure, | ||||
|                 exceptions.HTTPError, | ||||
|                 exceptions.HttpError, | ||||
|                 exceptions.ConnectionError): | ||||
|             LOG.warn(_LW('Discovering versions from the identity service ' | ||||
|                          'failed when creating the password plugin. ' | ||||
|   | ||||
| @@ -1,105 +0,0 @@ | ||||
| # Copyright 2010 Jacob Kaplan-Moss | ||||
| # Copyright 2011 Nebula, Inc. | ||||
| # | ||||
| #    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. | ||||
| """ | ||||
| Exception definitions. | ||||
|  | ||||
| .. py:exception:: AuthorizationFailure | ||||
|  | ||||
| .. py:exception:: ClientException | ||||
|  | ||||
| .. py:exception:: HttpError | ||||
|  | ||||
| .. py:exception:: ValidationError | ||||
|  | ||||
| .. py:exception:: Unauthorized | ||||
|  | ||||
| """ | ||||
|  | ||||
| from keystoneauth.i18n import _ | ||||
| from keystoneauth.openstack.common.apiclient.exceptions import *  # noqa | ||||
|  | ||||
| # NOTE(akurilin): This alias should be left here to support backwards | ||||
| # compatibility until we are sure that usage of these exceptions in | ||||
| # projects is correct. | ||||
| ConnectionError = ConnectionRefused | ||||
| HTTPNotImplemented = HttpNotImplemented | ||||
| Timeout = RequestTimeout | ||||
| HTTPError = HttpError | ||||
|  | ||||
|  | ||||
| class CertificateConfigError(Exception): | ||||
|     """Error reading the certificate.""" | ||||
|     def __init__(self, output): | ||||
|         self.output = output | ||||
|         msg = _('Unable to load certificate.') | ||||
|         super(CertificateConfigError, self).__init__(msg) | ||||
|  | ||||
|  | ||||
| class CMSError(Exception): | ||||
|     """Error reading the certificate.""" | ||||
|     def __init__(self, output): | ||||
|         self.output = output | ||||
|         msg = _('Unable to sign or verify data.') | ||||
|         super(CMSError, self).__init__(msg) | ||||
|  | ||||
|  | ||||
| class EmptyCatalog(EndpointNotFound): | ||||
|     """The service catalog is empty.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class SSLError(ConnectionRefused): | ||||
|     """An SSL error occurred.""" | ||||
|  | ||||
|  | ||||
| class DiscoveryFailure(ClientException): | ||||
|     """Discovery of client versions failed.""" | ||||
|  | ||||
|  | ||||
| class VersionNotAvailable(DiscoveryFailure): | ||||
|     """Discovery failed as the version you requested is not available.""" | ||||
|  | ||||
|  | ||||
| class MethodNotImplemented(ClientException): | ||||
|     """Method not implemented by the keystonauth API.""" | ||||
|  | ||||
|  | ||||
| class MissingAuthPlugin(ClientException): | ||||
|     """An authenticated request is required but no plugin available.""" | ||||
|  | ||||
|  | ||||
| class NoMatchingPlugin(ClientException): | ||||
|     """There were no auth plugins that could be created from the parameters | ||||
|     provided. | ||||
|  | ||||
|     :param str name: The name of the plugin that was attempted to load. | ||||
|  | ||||
|     .. py:attribute:: name | ||||
|  | ||||
|         The name of the plugin that was attempted to load. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name): | ||||
|         self.name = name | ||||
|         msg = _('The plugin %s could not be found') % name | ||||
|         super(NoMatchingPlugin, self).__init__(msg) | ||||
|  | ||||
|  | ||||
| class InvalidResponse(ClientException): | ||||
|     """The response from the server is not valid for this request.""" | ||||
|  | ||||
|     def __init__(self, response): | ||||
|         super(InvalidResponse, self).__init__() | ||||
|         self.response = response | ||||
							
								
								
									
										21
									
								
								keystoneauth/exceptions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								keystoneauth/exceptions/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # 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. | ||||
|  | ||||
|  | ||||
| from keystoneauth.exceptions.auth import *  # noqa | ||||
| from keystoneauth.exceptions.auth_plugins import *  # noqa | ||||
| from keystoneauth.exceptions.base import *  # noqa | ||||
| from keystoneauth.exceptions.catalog import *  # noqa | ||||
| from keystoneauth.exceptions.connection import *  # noqa | ||||
| from keystoneauth.exceptions.discovery import *  # noqa | ||||
| from keystoneauth.exceptions.http import *  # noqa | ||||
| from keystoneauth.exceptions.response import *  # noqa | ||||
							
								
								
									
										18
									
								
								keystoneauth/exceptions/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								keystoneauth/exceptions/auth.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # 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. | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
|  | ||||
|  | ||||
| class AuthorizationFailure(base.ClientException): | ||||
|     """Cannot authorize API client.""" | ||||
|     pass | ||||
							
								
								
									
										39
									
								
								keystoneauth/exceptions/auth_plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								keystoneauth/exceptions/auth_plugins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| # 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. | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
| from keystoneauth.i18n import _ | ||||
|  | ||||
|  | ||||
| class AuthPluginException(base.ClientException): | ||||
|     """Something went wrong with auth plugins.""" | ||||
|  | ||||
|  | ||||
| class MissingAuthPlugin(AuthPluginException): | ||||
|     """An authenticated request is required but no plugin available.""" | ||||
|  | ||||
|  | ||||
| class NoMatchingPlugin(AuthPluginException): | ||||
|     """There were no auth plugins that could be created from the parameters | ||||
|     provided. | ||||
|  | ||||
|     :param str name: The name of the plugin that was attempted to load. | ||||
|  | ||||
|     .. py:attribute:: name | ||||
|  | ||||
|         The name of the plugin that was attempted to load. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, name): | ||||
|         self.name = name | ||||
|         msg = _('The plugin %s could not be found') % name | ||||
|         super(NoMatchingPlugin, self).__init__(msg) | ||||
							
								
								
									
										18
									
								
								keystoneauth/exceptions/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								keystoneauth/exceptions/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # 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. | ||||
|  | ||||
|  | ||||
| __all__ = ['ClientException'] | ||||
|  | ||||
|  | ||||
| class ClientException(Exception): | ||||
|     """The base exception for everything to do with clients.""" | ||||
							
								
								
									
										32
									
								
								keystoneauth/exceptions/catalog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								keystoneauth/exceptions/catalog.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| # 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. | ||||
|  | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
|  | ||||
| __all__ = ['CatalogException', | ||||
|            'EmptyCatalog', | ||||
|            'EndpointNotFound'] | ||||
|  | ||||
|  | ||||
| class CatalogException(base.ClientException): | ||||
|     """Something is rotten in Service Catalog.""" | ||||
|  | ||||
|  | ||||
| class EmptyCatalog(CatalogException): | ||||
|     """The service catalog is empty.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class EndpointNotFound(CatalogException): | ||||
|     """Could not find requested endpoint in Service Catalog.""" | ||||
|     pass | ||||
							
								
								
									
										53
									
								
								keystoneauth/exceptions/connection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								keystoneauth/exceptions/connection.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| # 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. | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
|  | ||||
|  | ||||
| __all__ = ['ConnectionError', | ||||
|            'ConnectTimeout', | ||||
|            'ConnectFailure', | ||||
|            'SSLError', | ||||
|            'RetriableConnectionFailure'] | ||||
|  | ||||
|  | ||||
| class RetriableConnectionFailure(Exception): | ||||
|     """A mixin class that implies you can retry the most recent request.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ConnectionError(base.ClientException): | ||||
|     """Cannot connect to API service.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ConnectTimeout(ConnectionError, RetriableConnectionFailure): | ||||
|     """Timed out connecting to service""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ConnectFailure(ConnectionError, RetriableConnectionFailure): | ||||
|     """A retryable connection failure.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class SSLError(ConnectionError): | ||||
|     """An SSL error occurred.""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class UnknownConnectionError(ConnectionError): | ||||
|     """An error was encountered but we don't know what it is.""" | ||||
|  | ||||
|     def __init__(self, msg, original): | ||||
|         super(UnknownConnectionError, self).__init__(msg) | ||||
|         self.original = original | ||||
							
								
								
									
										25
									
								
								keystoneauth/exceptions/discovery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								keystoneauth/exceptions/discovery.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # 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. | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
|  | ||||
|  | ||||
| __all__ = ['DiscoveryFailure', | ||||
|            'VersionNotAvailable'] | ||||
|  | ||||
|  | ||||
| class DiscoveryFailure(base.ClientException): | ||||
|     """Discovery of client versions failed.""" | ||||
|  | ||||
|  | ||||
| class VersionNotAvailable(DiscoveryFailure): | ||||
|     """Discovery failed as the version you requested is not available.""" | ||||
| @@ -17,7 +17,7 @@ | ||||
| #    under the License. | ||||
| 
 | ||||
| """ | ||||
| Exception definitions. | ||||
| HTTP Exceptions used by keystoneauth | ||||
| """ | ||||
| 
 | ||||
| import inspect | ||||
| @@ -25,81 +25,45 @@ import sys | ||||
| 
 | ||||
| import six | ||||
| 
 | ||||
| from keystoneauth.openstack.common._i18n import _ | ||||
| from keystoneauth.exceptions import base | ||||
| from keystoneauth.i18n import _ | ||||
| 
 | ||||
| 
 | ||||
| class ClientException(Exception): | ||||
|     """The base exception class for all exceptions this library raises. | ||||
|     """ | ||||
|     pass | ||||
| __all__ = ['HttpError', | ||||
| 
 | ||||
|            'HTTPClientError', | ||||
|            'BadRequest', | ||||
|            'Unauthorized', | ||||
|            'PaymentRequired', | ||||
|            'Forbidden', | ||||
|            'NotFound', | ||||
|            'MethodNotAllowed', | ||||
|            'NotAcceptable', | ||||
|            'ProxyAuthenticationRequired', | ||||
|            'RequestTimeout', | ||||
|            'Conflict', | ||||
|            'Gone', | ||||
|            'LengthRequired', | ||||
|            'PreconditionFailed', | ||||
|            'RequestEntityTooLarge', | ||||
|            'RequestUriTooLong', | ||||
|            'UnsupportedMediaType', | ||||
|            'RequestedRangeNotSatisfiable', | ||||
|            'ExpectationFailed', | ||||
|            'UnprocessableEntity', | ||||
| 
 | ||||
|            'HttpServerError', | ||||
|            'InternalServerError', | ||||
|            'HttpNotImplemented', | ||||
|            'BadGateway', | ||||
|            'ServiceUnavailable', | ||||
|            'GatewayTimeout', | ||||
|            'HttpVersionNotSupported', | ||||
| 
 | ||||
|            'from_response'] | ||||
| 
 | ||||
| 
 | ||||
| class ValidationError(ClientException): | ||||
|     """Error in validation on API client side.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class UnsupportedVersion(ClientException): | ||||
|     """User is trying to use an unsupported version of the API.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class CommandError(ClientException): | ||||
|     """Error in CLI tool.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class AuthorizationFailure(ClientException): | ||||
|     """Cannot authorize API client.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ConnectionRefused(ClientException): | ||||
|     """Cannot connect to API service.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class AuthPluginOptionsMissing(AuthorizationFailure): | ||||
|     """Auth plugin misses some options.""" | ||||
|     def __init__(self, opt_names): | ||||
|         super(AuthPluginOptionsMissing, self).__init__( | ||||
|             _("Authentication failed. Missing options: %s") % | ||||
|             ", ".join(opt_names)) | ||||
|         self.opt_names = opt_names | ||||
| 
 | ||||
| 
 | ||||
| class AuthSystemNotFound(AuthorizationFailure): | ||||
|     """User has specified an AuthSystem that is not installed.""" | ||||
|     def __init__(self, auth_system): | ||||
|         super(AuthSystemNotFound, self).__init__( | ||||
|             _("AuthSystemNotFound: %s") % repr(auth_system)) | ||||
|         self.auth_system = auth_system | ||||
| 
 | ||||
| 
 | ||||
| class NoUniqueMatch(ClientException): | ||||
|     """Multiple entities found instead of one.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class EndpointException(ClientException): | ||||
|     """Something is rotten in Service Catalog.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class EndpointNotFound(EndpointException): | ||||
|     """Could not find requested endpoint in Service Catalog.""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class AmbiguousEndpoints(EndpointException): | ||||
|     """Found more than one matching endpoint in Service Catalog.""" | ||||
|     def __init__(self, endpoints=None): | ||||
|         super(AmbiguousEndpoints, self).__init__( | ||||
|             _("AmbiguousEndpoints: %s") % repr(endpoints)) | ||||
|         self.endpoints = endpoints | ||||
| 
 | ||||
| 
 | ||||
| class HttpError(ClientException): | ||||
| class HttpError(base.ClientException): | ||||
|     """The base exception class for all HTTP exceptions. | ||||
|     """ | ||||
|     http_status = 0 | ||||
| @@ -121,11 +85,6 @@ class HttpError(ClientException): | ||||
|         super(HttpError, self).__init__(formatted_string) | ||||
| 
 | ||||
| 
 | ||||
| class HTTPRedirection(HttpError): | ||||
|     """HTTP Redirection.""" | ||||
|     message = _("HTTP Redirection") | ||||
| 
 | ||||
| 
 | ||||
| class HTTPClientError(HttpError): | ||||
|     """Client-side HTTP error. | ||||
| 
 | ||||
| @@ -143,16 +102,6 @@ class HttpServerError(HttpError): | ||||
|     message = _("HTTP Server Error") | ||||
| 
 | ||||
| 
 | ||||
| class MultipleChoices(HTTPRedirection): | ||||
|     """HTTP 300 - Multiple Choices. | ||||
| 
 | ||||
|     Indicates multiple options for the resource that the client may follow. | ||||
|     """ | ||||
| 
 | ||||
|     http_status = 300 | ||||
|     message = _("Multiple Choices") | ||||
| 
 | ||||
| 
 | ||||
| class BadRequest(HTTPClientError): | ||||
|     """HTTP 400 - Bad Request. | ||||
| 
 | ||||
| @@ -417,11 +366,8 @@ def from_response(response, method, url): | ||||
|     :param method: HTTP method used for request | ||||
|     :param url: URL used for request | ||||
|     """ | ||||
| 
 | ||||
|     req_id = response.headers.get("x-openstack-request-id") | ||||
|     # NOTE(hdd) true for older versions of nova and cinder | ||||
|     if not req_id: | ||||
|         req_id = response.headers.get("x-compute-request-id") | ||||
| 
 | ||||
|     kwargs = { | ||||
|         "http_status": response.status_code, | ||||
|         "response": response, | ||||
							
								
								
									
										25
									
								
								keystoneauth/exceptions/response.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								keystoneauth/exceptions/response.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # 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. | ||||
|  | ||||
|  | ||||
| from keystoneauth.exceptions import base | ||||
|  | ||||
|  | ||||
| __all__ = ['InvalidResponse'] | ||||
|  | ||||
|  | ||||
| class InvalidResponse(base.ClientException): | ||||
|     """The response from the server is not valid for this request.""" | ||||
|  | ||||
|     def __init__(self, response): | ||||
|         super(InvalidResponse, self).__init__() | ||||
|         self.response = response | ||||
| @@ -1,40 +0,0 @@ | ||||
| #    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. | ||||
|  | ||||
| """oslo.i18n integration module. | ||||
|  | ||||
| See http://docs.openstack.org/developer/oslo.i18n/usage.html | ||||
|  | ||||
| """ | ||||
|  | ||||
| import oslo.i18n | ||||
|  | ||||
|  | ||||
| # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the | ||||
| # application name when this module is synced into the separate | ||||
| # repository. It is OK to have more than one translation function | ||||
| # using the same domain, since there will still only be one message | ||||
| # catalog. | ||||
| _translators = oslo.i18n.TranslatorFactory(domain='keystonauth') | ||||
|  | ||||
| # The primary translation function using the well-known name "_" | ||||
| _ = _translators.primary | ||||
|  | ||||
| # Translators for log levels. | ||||
| # | ||||
| # The abbreviated names are meant to reflect the usual use of a short | ||||
| # name like '_'. The "L" is for "log" and the other letter comes from | ||||
| # the level. | ||||
| _LI = _translators.log_info | ||||
| _LW = _translators.log_warning | ||||
| _LE = _translators.log_error | ||||
| _LC = _translators.log_critical | ||||
| @@ -1,221 +0,0 @@ | ||||
| # Copyright 2013 OpenStack Foundation | ||||
| # Copyright 2013 Spanish National Research Council. | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| # E0202: An attribute inherited from %s hide this method | ||||
| # pylint: disable=E0202 | ||||
|  | ||||
| import abc | ||||
| import argparse | ||||
| import os | ||||
|  | ||||
| import six | ||||
| from stevedore import extension | ||||
|  | ||||
| from keystoneauth.openstack.common.apiclient import exceptions | ||||
|  | ||||
|  | ||||
| _discovered_plugins = {} | ||||
|  | ||||
|  | ||||
| def discover_auth_systems(): | ||||
|     """Discover the available auth-systems. | ||||
|  | ||||
|     This won't take into account the old style auth-systems. | ||||
|     """ | ||||
|     global _discovered_plugins | ||||
|     _discovered_plugins = {} | ||||
|  | ||||
|     def add_plugin(ext): | ||||
|         _discovered_plugins[ext.name] = ext.plugin | ||||
|  | ||||
|     ep_namespace = "keystonauth.openstack.common.apiclient.auth" | ||||
|     mgr = extension.ExtensionManager(ep_namespace) | ||||
|     mgr.map(add_plugin) | ||||
|  | ||||
|  | ||||
| def load_auth_system_opts(parser): | ||||
|     """Load options needed by the available auth-systems into a parser. | ||||
|  | ||||
|     This function will try to populate the parser with options from the | ||||
|     available plugins. | ||||
|     """ | ||||
|     group = parser.add_argument_group("Common auth options") | ||||
|     BaseAuthPlugin.add_common_opts(group) | ||||
|     for name, auth_plugin in six.iteritems(_discovered_plugins): | ||||
|         group = parser.add_argument_group( | ||||
|             "Auth-system '%s' options" % name, | ||||
|             conflict_handler="resolve") | ||||
|         auth_plugin.add_opts(group) | ||||
|  | ||||
|  | ||||
| def load_plugin(auth_system): | ||||
|     try: | ||||
|         plugin_class = _discovered_plugins[auth_system] | ||||
|     except KeyError: | ||||
|         raise exceptions.AuthSystemNotFound(auth_system) | ||||
|     return plugin_class(auth_system=auth_system) | ||||
|  | ||||
|  | ||||
| def load_plugin_from_args(args): | ||||
|     """Load required plugin and populate it with options. | ||||
|  | ||||
|     Try to guess auth system if it is not specified. Systems are tried in | ||||
|     alphabetical order. | ||||
|  | ||||
|     :type args: argparse.Namespace | ||||
|     :raises: AuthPluginOptionsMissing | ||||
|     """ | ||||
|     auth_system = args.os_auth_system | ||||
|     if auth_system: | ||||
|         plugin = load_plugin(auth_system) | ||||
|         plugin.parse_opts(args) | ||||
|         plugin.sufficient_options() | ||||
|         return plugin | ||||
|  | ||||
|     for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): | ||||
|         plugin_class = _discovered_plugins[plugin_auth_system] | ||||
|         plugin = plugin_class() | ||||
|         plugin.parse_opts(args) | ||||
|         try: | ||||
|             plugin.sufficient_options() | ||||
|         except exceptions.AuthPluginOptionsMissing: | ||||
|             continue | ||||
|         return plugin | ||||
|     raise exceptions.AuthPluginOptionsMissing(["auth_system"]) | ||||
|  | ||||
|  | ||||
| @six.add_metaclass(abc.ABCMeta) | ||||
| class BaseAuthPlugin(object): | ||||
|     """Base class for authentication plugins. | ||||
|  | ||||
|     An authentication plugin needs to override at least the authenticate | ||||
|     method to be a valid plugin. | ||||
|     """ | ||||
|  | ||||
|     auth_system = None | ||||
|     opt_names = [] | ||||
|     common_opt_names = [ | ||||
|         "auth_system", | ||||
|         "username", | ||||
|         "password", | ||||
|         "tenant_name", | ||||
|         "token", | ||||
|         "auth_url", | ||||
|     ] | ||||
|  | ||||
|     def __init__(self, auth_system=None, **kwargs): | ||||
|         self.auth_system = auth_system or self.auth_system | ||||
|         self.opts = dict((name, kwargs.get(name)) | ||||
|                          for name in self.opt_names) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _parser_add_opt(parser, opt): | ||||
|         """Add an option to parser in two variants. | ||||
|  | ||||
|         :param opt: option name (with underscores) | ||||
|         """ | ||||
|         dashed_opt = opt.replace("_", "-") | ||||
|         env_var = "OS_%s" % opt.upper() | ||||
|         arg_default = os.environ.get(env_var, "") | ||||
|         arg_help = "Defaults to env[%s]." % env_var | ||||
|         parser.add_argument( | ||||
|             "--os-%s" % dashed_opt, | ||||
|             metavar="<%s>" % dashed_opt, | ||||
|             default=arg_default, | ||||
|             help=arg_help) | ||||
|         parser.add_argument( | ||||
|             "--os_%s" % opt, | ||||
|             metavar="<%s>" % dashed_opt, | ||||
|             help=argparse.SUPPRESS) | ||||
|  | ||||
|     @classmethod | ||||
|     def add_opts(cls, parser): | ||||
|         """Populate the parser with the options for this plugin. | ||||
|         """ | ||||
|         for opt in cls.opt_names: | ||||
|             # use `BaseAuthPlugin.common_opt_names` since it is never | ||||
|             # changed in child classes | ||||
|             if opt not in BaseAuthPlugin.common_opt_names: | ||||
|                 cls._parser_add_opt(parser, opt) | ||||
|  | ||||
|     @classmethod | ||||
|     def add_common_opts(cls, parser): | ||||
|         """Add options that are common for several plugins. | ||||
|         """ | ||||
|         for opt in cls.common_opt_names: | ||||
|             cls._parser_add_opt(parser, opt) | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_opt(opt_name, args): | ||||
|         """Return option name and value. | ||||
|  | ||||
|         :param opt_name: name of the option, e.g., "username" | ||||
|         :param args: parsed arguments | ||||
|         """ | ||||
|         return (opt_name, getattr(args, "os_%s" % opt_name, None)) | ||||
|  | ||||
|     def parse_opts(self, args): | ||||
|         """Parse the actual auth-system options if any. | ||||
|  | ||||
|         This method is expected to populate the attribute `self.opts` with a | ||||
|         dict containing the options and values needed to make authentication. | ||||
|         """ | ||||
|         self.opts.update(dict(self.get_opt(opt_name, args) | ||||
|                               for opt_name in self.opt_names)) | ||||
|  | ||||
|     def authenticate(self, http_client): | ||||
|         """Authenticate using plugin defined method. | ||||
|  | ||||
|         The method usually analyses `self.opts` and performs | ||||
|         a request to authentication server. | ||||
|  | ||||
|         :param http_client: client object that needs authentication | ||||
|         :type http_client: HTTPClient | ||||
|         :raises: AuthorizationFailure | ||||
|         """ | ||||
|         self.sufficient_options() | ||||
|         self._do_authenticate(http_client) | ||||
|  | ||||
|     @abc.abstractmethod | ||||
|     def _do_authenticate(self, http_client): | ||||
|         """Protected method for authentication. | ||||
|         """ | ||||
|  | ||||
|     def sufficient_options(self): | ||||
|         """Check if all required options are present. | ||||
|  | ||||
|         :raises: AuthPluginOptionsMissing | ||||
|         """ | ||||
|         missing = [opt | ||||
|                    for opt in self.opt_names | ||||
|                    if not self.opts.get(opt)] | ||||
|         if missing: | ||||
|             raise exceptions.AuthPluginOptionsMissing(missing) | ||||
|  | ||||
|     @abc.abstractmethod | ||||
|     def token_and_endpoint(self, endpoint_type, service_type): | ||||
|         """Return token and endpoint. | ||||
|  | ||||
|         :param service_type: Service type of the endpoint | ||||
|         :type service_type: string | ||||
|         :param endpoint_type: Type of endpoint. | ||||
|                               Possible values: public or publicURL, | ||||
|                               internal or internalURL, | ||||
|                               admin or adminURL | ||||
|         :type endpoint_type: string | ||||
|         :returns: tuple of token and endpoint strings | ||||
|         :raises: EndpointException | ||||
|         """ | ||||
| @@ -1,518 +0,0 @@ | ||||
| # Copyright 2010 Jacob Kaplan-Moss | ||||
| # Copyright 2011 OpenStack Foundation | ||||
| # Copyright 2012 Grid Dynamics | ||||
| # Copyright 2013 OpenStack Foundation | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| """ | ||||
| Base utilities to build API operation managers and objects on top of. | ||||
| """ | ||||
|  | ||||
| # E1102: %s is not callable | ||||
| # pylint: disable=E1102 | ||||
|  | ||||
| import abc | ||||
| import copy | ||||
|  | ||||
| from oslo.utils import strutils | ||||
| import six | ||||
| from six.moves.urllib import parse | ||||
|  | ||||
| from keystoneauth.openstack.common._i18n import _ | ||||
| from keystoneauth.openstack.common.apiclient import exceptions | ||||
|  | ||||
|  | ||||
| def getid(obj): | ||||
|     """Return id if argument is a Resource. | ||||
|  | ||||
|     Abstracts the common pattern of allowing both an object or an object's ID | ||||
|     (UUID) as a parameter when dealing with relationships. | ||||
|     """ | ||||
|     try: | ||||
|         if obj.uuid: | ||||
|             return obj.uuid | ||||
|     except AttributeError: | ||||
|         pass | ||||
|     try: | ||||
|         return obj.id | ||||
|     except AttributeError: | ||||
|         return obj | ||||
|  | ||||
|  | ||||
| # TODO(aababilov): call run_hooks() in HookableMixin's child classes | ||||
| class HookableMixin(object): | ||||
|     """Mixin so classes can register and run hooks.""" | ||||
|     _hooks_map = {} | ||||
|  | ||||
|     @classmethod | ||||
|     def add_hook(cls, hook_type, hook_func): | ||||
|         """Add a new hook of specified type. | ||||
|  | ||||
|         :param cls: class that registers hooks | ||||
|         :param hook_type: hook type, e.g., '__pre_parse_args__' | ||||
|         :param hook_func: hook function | ||||
|         """ | ||||
|         if hook_type not in cls._hooks_map: | ||||
|             cls._hooks_map[hook_type] = [] | ||||
|  | ||||
|         cls._hooks_map[hook_type].append(hook_func) | ||||
|  | ||||
|     @classmethod | ||||
|     def run_hooks(cls, hook_type, *args, **kwargs): | ||||
|         """Run all hooks of specified type. | ||||
|  | ||||
|         :param cls: class that registers hooks | ||||
|         :param hook_type: hook type, e.g., '__pre_parse_args__' | ||||
|         :param args: args to be passed to every hook function | ||||
|         :param kwargs: kwargs to be passed to every hook function | ||||
|         """ | ||||
|         hook_funcs = cls._hooks_map.get(hook_type) or [] | ||||
|         for hook_func in hook_funcs: | ||||
|             hook_func(*args, **kwargs) | ||||
|  | ||||
|  | ||||
| class BaseManager(HookableMixin): | ||||
|     """Basic manager type providing common operations. | ||||
|  | ||||
|     Managers interact with a particular type of API (servers, flavors, images, | ||||
|     etc.) and provide CRUD operations for them. | ||||
|     """ | ||||
|     resource_class = None | ||||
|  | ||||
|     def __init__(self, client): | ||||
|         """Initializes BaseManager with `client`. | ||||
|  | ||||
|         :param client: instance of BaseClient descendant for HTTP requests | ||||
|         """ | ||||
|         super(BaseManager, self).__init__() | ||||
|         self.client = client | ||||
|  | ||||
|     def _list(self, url, response_key=None, obj_class=None, json=None): | ||||
|         """List the collection. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         :param response_key: the key to be looked up in response dictionary, | ||||
|             e.g., 'servers'. If response_key is None - all response body | ||||
|             will be used. | ||||
|         :param obj_class: class for constructing the returned objects | ||||
|             (self.resource_class will be used by default) | ||||
|         :param json: data that will be encoded as JSON and passed in POST | ||||
|             request (GET will be sent by default) | ||||
|         """ | ||||
|         if json: | ||||
|             body = self.client.post(url, json=json).json() | ||||
|         else: | ||||
|             body = self.client.get(url).json() | ||||
|  | ||||
|         if obj_class is None: | ||||
|             obj_class = self.resource_class | ||||
|  | ||||
|         data = body[response_key] if response_key is not None else body | ||||
|         # NOTE(ja): keystone returns values as list as {'values': [ ... ]} | ||||
|         #           unlike other services which just return the list... | ||||
|         try: | ||||
|             data = data['values'] | ||||
|         except (KeyError, TypeError): | ||||
|             pass | ||||
|  | ||||
|         return [obj_class(self, res, loaded=True) for res in data if res] | ||||
|  | ||||
|     def _get(self, url, response_key=None): | ||||
|         """Get an object from collection. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         :param response_key: the key to be looked up in response dictionary, | ||||
|             e.g., 'server'. If response_key is None - all response body | ||||
|             will be used. | ||||
|         """ | ||||
|         body = self.client.get(url).json() | ||||
|         data = body[response_key] if response_key is not None else body | ||||
|         return self.resource_class(self, data, loaded=True) | ||||
|  | ||||
|     def _head(self, url): | ||||
|         """Retrieve request headers for an object. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         """ | ||||
|         resp = self.client.head(url) | ||||
|         return resp.status_code == 204 | ||||
|  | ||||
|     def _post(self, url, json, response_key=None, return_raw=False): | ||||
|         """Create an object. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         :param json: data that will be encoded as JSON and passed in POST | ||||
|             request (GET will be sent by default) | ||||
|         :param response_key: the key to be looked up in response dictionary, | ||||
|             e.g., 'server'. If response_key is None - all response body | ||||
|             will be used. | ||||
|         :param return_raw: flag to force returning raw JSON instead of | ||||
|             Python object of self.resource_class | ||||
|         """ | ||||
|         body = self.client.post(url, json=json).json() | ||||
|         data = body[response_key] if response_key is not None else body | ||||
|         if return_raw: | ||||
|             return data | ||||
|         return self.resource_class(self, data) | ||||
|  | ||||
|     def _put(self, url, json=None, response_key=None): | ||||
|         """Update an object with PUT method. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         :param json: data that will be encoded as JSON and passed in POST | ||||
|             request (GET will be sent by default) | ||||
|         :param response_key: the key to be looked up in response dictionary, | ||||
|             e.g., 'servers'. If response_key is None - all response body | ||||
|             will be used. | ||||
|         """ | ||||
|         resp = self.client.put(url, json=json) | ||||
|         # PUT requests may not return a body | ||||
|         if resp.content: | ||||
|             body = resp.json() | ||||
|             if response_key is not None: | ||||
|                 return self.resource_class(self, body[response_key]) | ||||
|             else: | ||||
|                 return self.resource_class(self, body) | ||||
|  | ||||
|     def _patch(self, url, json=None, response_key=None): | ||||
|         """Update an object with PATCH method. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers' | ||||
|         :param json: data that will be encoded as JSON and passed in POST | ||||
|             request (GET will be sent by default) | ||||
|         :param response_key: the key to be looked up in response dictionary, | ||||
|             e.g., 'servers'. If response_key is None - all response body | ||||
|             will be used. | ||||
|         """ | ||||
|         body = self.client.patch(url, json=json).json() | ||||
|         if response_key is not None: | ||||
|             return self.resource_class(self, body[response_key]) | ||||
|         else: | ||||
|             return self.resource_class(self, body) | ||||
|  | ||||
|     def _delete(self, url): | ||||
|         """Delete an object. | ||||
|  | ||||
|         :param url: a partial URL, e.g., '/servers/my-server' | ||||
|         """ | ||||
|         return self.client.delete(url) | ||||
|  | ||||
|  | ||||
| @six.add_metaclass(abc.ABCMeta) | ||||
| class ManagerWithFind(BaseManager): | ||||
|     """Manager with additional `find()`/`findall()` methods.""" | ||||
|  | ||||
|     @abc.abstractmethod | ||||
|     def list(self): | ||||
|         pass | ||||
|  | ||||
|     def find(self, **kwargs): | ||||
|         """Find a single item with attributes matching ``**kwargs``. | ||||
|  | ||||
|         This isn't very efficient: it loads the entire list then filters on | ||||
|         the Python side. | ||||
|         """ | ||||
|         matches = self.findall(**kwargs) | ||||
|         num_matches = len(matches) | ||||
|         if num_matches == 0: | ||||
|             msg = _("No %(name)s matching %(args)s.") % { | ||||
|                 'name': self.resource_class.__name__, | ||||
|                 'args': kwargs | ||||
|             } | ||||
|             raise exceptions.NotFound(msg) | ||||
|         elif num_matches > 1: | ||||
|             raise exceptions.NoUniqueMatch() | ||||
|         else: | ||||
|             return matches[0] | ||||
|  | ||||
|     def findall(self, **kwargs): | ||||
|         """Find all items with attributes matching ``**kwargs``. | ||||
|  | ||||
|         This isn't very efficient: it loads the entire list then filters on | ||||
|         the Python side. | ||||
|         """ | ||||
|         found = [] | ||||
|         searches = kwargs.items() | ||||
|  | ||||
|         for obj in self.list(): | ||||
|             try: | ||||
|                 if all(getattr(obj, attr) == value | ||||
|                        for (attr, value) in searches): | ||||
|                     found.append(obj) | ||||
|             except AttributeError: | ||||
|                 continue | ||||
|  | ||||
|         return found | ||||
|  | ||||
|  | ||||
| class CrudManager(BaseManager): | ||||
|     """Base manager class for manipulating entities. | ||||
|  | ||||
|     Children of this class are expected to define a `collection_key` and `key`. | ||||
|  | ||||
|     - `collection_key`: Usually a plural noun by convention (e.g. `entities`); | ||||
|       used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON | ||||
|       objects containing a list of member resources (e.g. `{'entities': [{}, | ||||
|       {}, {}]}`). | ||||
|     - `key`: Usually a singular noun by convention (e.g. `entity`); used to | ||||
|       refer to an individual member of the collection. | ||||
|  | ||||
|     """ | ||||
|     collection_key = None | ||||
|     key = None | ||||
|  | ||||
|     def build_url(self, base_url=None, **kwargs): | ||||
|         """Builds a resource URL for the given kwargs. | ||||
|  | ||||
|         Given an example collection where `collection_key = 'entities'` and | ||||
|         `key = 'entity'`, the following URL's could be generated. | ||||
|  | ||||
|         By default, the URL will represent a collection of entities, e.g.:: | ||||
|  | ||||
|             /entities | ||||
|  | ||||
|         If kwargs contains an `entity_id`, then the URL will represent a | ||||
|         specific member, e.g.:: | ||||
|  | ||||
|             /entities/{entity_id} | ||||
|  | ||||
|         :param base_url: if provided, the generated URL will be appended to it | ||||
|         """ | ||||
|         url = base_url if base_url is not None else '' | ||||
|  | ||||
|         url += '/%s' % self.collection_key | ||||
|  | ||||
|         # do we have a specific entity? | ||||
|         entity_id = kwargs.get('%s_id' % self.key) | ||||
|         if entity_id is not None: | ||||
|             url += '/%s' % entity_id | ||||
|  | ||||
|         return url | ||||
|  | ||||
|     def _filter_kwargs(self, kwargs): | ||||
|         """Drop null values and handle ids.""" | ||||
|         for key, ref in six.iteritems(kwargs.copy()): | ||||
|             if ref is None: | ||||
|                 kwargs.pop(key) | ||||
|             else: | ||||
|                 if isinstance(ref, Resource): | ||||
|                     kwargs.pop(key) | ||||
|                     kwargs['%s_id' % key] = getid(ref) | ||||
|         return kwargs | ||||
|  | ||||
|     def create(self, **kwargs): | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|         return self._post( | ||||
|             self.build_url(**kwargs), | ||||
|             {self.key: kwargs}, | ||||
|             self.key) | ||||
|  | ||||
|     def get(self, **kwargs): | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|         return self._get( | ||||
|             self.build_url(**kwargs), | ||||
|             self.key) | ||||
|  | ||||
|     def head(self, **kwargs): | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|         return self._head(self.build_url(**kwargs)) | ||||
|  | ||||
|     def list(self, base_url=None, **kwargs): | ||||
|         """List the collection. | ||||
|  | ||||
|         :param base_url: if provided, the generated URL will be appended to it | ||||
|         """ | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|  | ||||
|         return self._list( | ||||
|             '%(base_url)s%(query)s' % { | ||||
|                 'base_url': self.build_url(base_url=base_url, **kwargs), | ||||
|                 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', | ||||
|             }, | ||||
|             self.collection_key) | ||||
|  | ||||
|     def put(self, base_url=None, **kwargs): | ||||
|         """Update an element. | ||||
|  | ||||
|         :param base_url: if provided, the generated URL will be appended to it | ||||
|         """ | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|  | ||||
|         return self._put(self.build_url(base_url=base_url, **kwargs)) | ||||
|  | ||||
|     def update(self, **kwargs): | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|         params = kwargs.copy() | ||||
|         params.pop('%s_id' % self.key) | ||||
|  | ||||
|         return self._patch( | ||||
|             self.build_url(**kwargs), | ||||
|             {self.key: params}, | ||||
|             self.key) | ||||
|  | ||||
|     def delete(self, **kwargs): | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|  | ||||
|         return self._delete( | ||||
|             self.build_url(**kwargs)) | ||||
|  | ||||
|     def find(self, base_url=None, **kwargs): | ||||
|         """Find a single item with attributes matching ``**kwargs``. | ||||
|  | ||||
|         :param base_url: if provided, the generated URL will be appended to it | ||||
|         """ | ||||
|         kwargs = self._filter_kwargs(kwargs) | ||||
|  | ||||
|         rl = self._list( | ||||
|             '%(base_url)s%(query)s' % { | ||||
|                 'base_url': self.build_url(base_url=base_url, **kwargs), | ||||
|                 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', | ||||
|             }, | ||||
|             self.collection_key) | ||||
|         num = len(rl) | ||||
|  | ||||
|         if num == 0: | ||||
|             msg = _("No %(name)s matching %(args)s.") % { | ||||
|                 'name': self.resource_class.__name__, | ||||
|                 'args': kwargs | ||||
|             } | ||||
|             raise exceptions.NotFound(404, msg) | ||||
|         elif num > 1: | ||||
|             raise exceptions.NoUniqueMatch | ||||
|         else: | ||||
|             return rl[0] | ||||
|  | ||||
|  | ||||
| class Extension(HookableMixin): | ||||
|     """Extension descriptor.""" | ||||
|  | ||||
|     SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') | ||||
|     manager_class = None | ||||
|  | ||||
|     def __init__(self, name, module): | ||||
|         super(Extension, self).__init__() | ||||
|         self.name = name | ||||
|         self.module = module | ||||
|         self._parse_extension_module() | ||||
|  | ||||
|     def _parse_extension_module(self): | ||||
|         self.manager_class = None | ||||
|         for attr_name, attr_value in self.module.__dict__.items(): | ||||
|             if attr_name in self.SUPPORTED_HOOKS: | ||||
|                 self.add_hook(attr_name, attr_value) | ||||
|             else: | ||||
|                 try: | ||||
|                     if issubclass(attr_value, BaseManager): | ||||
|                         self.manager_class = attr_value | ||||
|                 except TypeError: | ||||
|                     pass | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return "<Extension '%s'>" % self.name | ||||
|  | ||||
|  | ||||
| class Resource(object): | ||||
|     """Base class for OpenStack resources (tenant, user, etc.). | ||||
|  | ||||
|     This is pretty much just a bag for attributes. | ||||
|     """ | ||||
|  | ||||
|     HUMAN_ID = False | ||||
|     NAME_ATTR = 'name' | ||||
|  | ||||
|     def __init__(self, manager, info, loaded=False): | ||||
|         """Populate and bind to a manager. | ||||
|  | ||||
|         :param manager: BaseManager object | ||||
|         :param info: dictionary representing resource attributes | ||||
|         :param loaded: prevent lazy-loading if set to True | ||||
|         """ | ||||
|         self.manager = manager | ||||
|         self._info = info | ||||
|         self._add_details(info) | ||||
|         self._loaded = loaded | ||||
|  | ||||
|     def __repr__(self): | ||||
|         reprkeys = sorted(k | ||||
|                           for k in self.__dict__.keys() | ||||
|                           if k[0] != '_' and k != 'manager') | ||||
|         info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) | ||||
|         return "<%s %s>" % (self.__class__.__name__, info) | ||||
|  | ||||
|     @property | ||||
|     def human_id(self): | ||||
|         """Human-readable ID which can be used for bash completion. | ||||
|         """ | ||||
|         if self.HUMAN_ID: | ||||
|             name = getattr(self, self.NAME_ATTR, None) | ||||
|             if name is not None: | ||||
|                 return strutils.to_slug(name) | ||||
|         return None | ||||
|  | ||||
|     def _add_details(self, info): | ||||
|         for (k, v) in six.iteritems(info): | ||||
|             try: | ||||
|                 setattr(self, k, v) | ||||
|                 self._info[k] = v | ||||
|             except AttributeError: | ||||
|                 # In this case we already defined the attribute on the class | ||||
|                 pass | ||||
|  | ||||
|     def __getattr__(self, k): | ||||
|         if k not in self.__dict__: | ||||
|             # NOTE(bcwaldon): disallow lazy-loading if already loaded once | ||||
|             if not self.is_loaded(): | ||||
|                 self.get() | ||||
|                 return self.__getattr__(k) | ||||
|  | ||||
|             raise AttributeError(k) | ||||
|         else: | ||||
|             return self.__dict__[k] | ||||
|  | ||||
|     def get(self): | ||||
|         """Support for lazy loading details. | ||||
|  | ||||
|         Some clients, such as novaclient have the option to lazy load the | ||||
|         details, details which can be loaded with this function. | ||||
|         """ | ||||
|         # set_loaded() first ... so if we have to bail, we know we tried. | ||||
|         self.set_loaded(True) | ||||
|         if not hasattr(self.manager, 'get'): | ||||
|             return | ||||
|  | ||||
|         new = self.manager.get(self.id) | ||||
|         if new: | ||||
|             self._add_details(new._info) | ||||
|             self._add_details( | ||||
|                 {'x_request_id': self.manager.client.last_request_id}) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         if not isinstance(other, Resource): | ||||
|             return NotImplemented | ||||
|         # two resources of different types are not equal | ||||
|         if not isinstance(other, self.__class__): | ||||
|             return False | ||||
|         if hasattr(self, 'id') and hasattr(other, 'id'): | ||||
|             return self.id == other.id | ||||
|         return self._info == other._info | ||||
|  | ||||
|     def is_loaded(self): | ||||
|         return self._loaded | ||||
|  | ||||
|     def set_loaded(self, val): | ||||
|         self._loaded = val | ||||
|  | ||||
|     def to_dict(self): | ||||
|         return copy.deepcopy(self._info) | ||||
| @@ -1,388 +0,0 @@ | ||||
| # Copyright 2010 Jacob Kaplan-Moss | ||||
| # Copyright 2011 OpenStack Foundation | ||||
| # Copyright 2011 Piston Cloud Computing, Inc. | ||||
| # Copyright 2013 Alessio Ababilov | ||||
| # Copyright 2013 Grid Dynamics | ||||
| # Copyright 2013 OpenStack Foundation | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| """ | ||||
| OpenStack Client interface. Handles the REST calls and responses. | ||||
| """ | ||||
|  | ||||
| # E0202: An attribute inherited from %s hide this method | ||||
| # pylint: disable=E0202 | ||||
|  | ||||
| import hashlib | ||||
| import logging | ||||
| import time | ||||
|  | ||||
| try: | ||||
|     import simplejson as json | ||||
| except ImportError: | ||||
|     import json | ||||
|  | ||||
| from oslo.utils import encodeutils | ||||
| from oslo.utils import importutils | ||||
| import requests | ||||
|  | ||||
| from keystoneauth.openstack.common._i18n import _ | ||||
| from keystoneauth.openstack.common.apiclient import exceptions | ||||
|  | ||||
| _logger = logging.getLogger(__name__) | ||||
| SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) | ||||
|  | ||||
|  | ||||
| class HTTPClient(object): | ||||
|     """This client handles sending HTTP requests to OpenStack servers. | ||||
|  | ||||
|     Features: | ||||
|  | ||||
|     - share authentication information between several clients to different | ||||
|       services (e.g., for compute and image clients); | ||||
|     - reissue authentication request for expired tokens; | ||||
|     - encode/decode JSON bodies; | ||||
|     - raise exceptions on HTTP errors; | ||||
|     - pluggable authentication; | ||||
|     - store authentication information in a keyring; | ||||
|     - store time spent for requests; | ||||
|     - register clients for particular services, so one can use | ||||
|       `http_client.identity` or `http_client.compute`; | ||||
|     - log requests and responses in a format that is easy to copy-and-paste | ||||
|       into terminal and send the same request with curl. | ||||
|     """ | ||||
|  | ||||
|     user_agent = "keystonauth.openstack.common.apiclient" | ||||
|  | ||||
|     def __init__(self, | ||||
|                  auth_plugin, | ||||
|                  region_name=None, | ||||
|                  endpoint_type="publicURL", | ||||
|                  original_ip=None, | ||||
|                  verify=True, | ||||
|                  cert=None, | ||||
|                  timeout=None, | ||||
|                  timings=False, | ||||
|                  keyring_saver=None, | ||||
|                  debug=False, | ||||
|                  user_agent=None, | ||||
|                  http=None): | ||||
|         self.auth_plugin = auth_plugin | ||||
|  | ||||
|         self.endpoint_type = endpoint_type | ||||
|         self.region_name = region_name | ||||
|  | ||||
|         self.original_ip = original_ip | ||||
|         self.timeout = timeout | ||||
|         self.verify = verify | ||||
|         self.cert = cert | ||||
|  | ||||
|         self.keyring_saver = keyring_saver | ||||
|         self.debug = debug | ||||
|         self.user_agent = user_agent or self.user_agent | ||||
|  | ||||
|         self.times = []  # [("item", starttime, endtime), ...] | ||||
|         self.timings = timings | ||||
|  | ||||
|         # requests within the same session can reuse TCP connections from pool | ||||
|         self.http = http or requests.Session() | ||||
|  | ||||
|         self.cached_token = None | ||||
|         self.last_request_id = None | ||||
|  | ||||
|     def _safe_header(self, name, value): | ||||
|         if name in SENSITIVE_HEADERS: | ||||
|             # because in python3 byte string handling is ... ug | ||||
|             v = value.encode('utf-8') | ||||
|             h = hashlib.sha1(v) | ||||
|             d = h.hexdigest() | ||||
|             return encodeutils.safe_decode(name), "{SHA1}%s" % d | ||||
|         else: | ||||
|             return (encodeutils.safe_decode(name), | ||||
|                     encodeutils.safe_decode(value)) | ||||
|  | ||||
|     def _http_log_req(self, method, url, kwargs): | ||||
|         if not self.debug: | ||||
|             return | ||||
|  | ||||
|         string_parts = [ | ||||
|             "curl -i", | ||||
|             "-X '%s'" % method, | ||||
|             "'%s'" % url, | ||||
|         ] | ||||
|  | ||||
|         for element in kwargs['headers']: | ||||
|             header = ("-H '%s: %s'" % | ||||
|                       self._safe_header(element, kwargs['headers'][element])) | ||||
|             string_parts.append(header) | ||||
|  | ||||
|         _logger.debug("REQ: %s" % " ".join(string_parts)) | ||||
|         if 'data' in kwargs: | ||||
|             _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) | ||||
|  | ||||
|     def _http_log_resp(self, resp): | ||||
|         if not self.debug: | ||||
|             return | ||||
|         _logger.debug( | ||||
|             "RESP: [%s] %s\n", | ||||
|             resp.status_code, | ||||
|             resp.headers) | ||||
|         if resp._content_consumed: | ||||
|             _logger.debug( | ||||
|                 "RESP BODY: %s\n", | ||||
|                 resp.text) | ||||
|  | ||||
|     def serialize(self, kwargs): | ||||
|         if kwargs.get('json') is not None: | ||||
|             kwargs['headers']['Content-Type'] = 'application/json' | ||||
|             kwargs['data'] = json.dumps(kwargs['json']) | ||||
|         try: | ||||
|             del kwargs['json'] | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|     def get_timings(self): | ||||
|         return self.times | ||||
|  | ||||
|     def reset_timings(self): | ||||
|         self.times = [] | ||||
|  | ||||
|     def request(self, method, url, **kwargs): | ||||
|         """Send an http request with the specified characteristics. | ||||
|  | ||||
|         Wrapper around `requests.Session.request` to handle tasks such as | ||||
|         setting headers, JSON encoding/decoding, and error handling. | ||||
|  | ||||
|         :param method: method of HTTP request | ||||
|         :param url: URL of HTTP request | ||||
|         :param kwargs: any other parameter that can be passed to | ||||
|              requests.Session.request (such as `headers`) or `json` | ||||
|              that will be encoded as JSON and used as `data` argument | ||||
|         """ | ||||
|         kwargs.setdefault("headers", {}) | ||||
|         kwargs["headers"]["User-Agent"] = self.user_agent | ||||
|         if self.original_ip: | ||||
|             kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( | ||||
|                 self.original_ip, self.user_agent) | ||||
|         if self.timeout is not None: | ||||
|             kwargs.setdefault("timeout", self.timeout) | ||||
|         kwargs.setdefault("verify", self.verify) | ||||
|         if self.cert is not None: | ||||
|             kwargs.setdefault("cert", self.cert) | ||||
|         self.serialize(kwargs) | ||||
|  | ||||
|         self._http_log_req(method, url, kwargs) | ||||
|         if self.timings: | ||||
|             start_time = time.time() | ||||
|         resp = self.http.request(method, url, **kwargs) | ||||
|         if self.timings: | ||||
|             self.times.append(("%s %s" % (method, url), | ||||
|                                start_time, time.time())) | ||||
|         self._http_log_resp(resp) | ||||
|  | ||||
|         self.last_request_id = resp.headers.get('x-openstack-request-id') | ||||
|  | ||||
|         if resp.status_code >= 400: | ||||
|             _logger.debug( | ||||
|                 "Request returned failure status: %s", | ||||
|                 resp.status_code) | ||||
|             raise exceptions.from_response(resp, method, url) | ||||
|  | ||||
|         return resp | ||||
|  | ||||
|     @staticmethod | ||||
|     def concat_url(endpoint, url): | ||||
|         """Concatenate endpoint and final URL. | ||||
|  | ||||
|         E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to | ||||
|         "http://keystone/v2.0/tokens". | ||||
|  | ||||
|         :param endpoint: the base URL | ||||
|         :param url: the final URL | ||||
|         """ | ||||
|         return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) | ||||
|  | ||||
|     def client_request(self, client, method, url, **kwargs): | ||||
|         """Send an http request using `client`'s endpoint and specified `url`. | ||||
|  | ||||
|         If request was rejected as unauthorized (possibly because the token is | ||||
|         expired), issue one authorization attempt and send the request once | ||||
|         again. | ||||
|  | ||||
|         :param client: instance of BaseClient descendant | ||||
|         :param method: method of HTTP request | ||||
|         :param url: URL of HTTP request | ||||
|         :param kwargs: any other parameter that can be passed to | ||||
|             `HTTPClient.request` | ||||
|         """ | ||||
|  | ||||
|         filter_args = { | ||||
|             "endpoint_type": client.endpoint_type or self.endpoint_type, | ||||
|             "service_type": client.service_type, | ||||
|         } | ||||
|         token, endpoint = (self.cached_token, client.cached_endpoint) | ||||
|         just_authenticated = False | ||||
|         if not (token and endpoint): | ||||
|             try: | ||||
|                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||
|                     **filter_args) | ||||
|             except exceptions.EndpointException: | ||||
|                 pass | ||||
|             if not (token and endpoint): | ||||
|                 self.authenticate() | ||||
|                 just_authenticated = True | ||||
|                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||
|                     **filter_args) | ||||
|                 if not (token and endpoint): | ||||
|                     raise exceptions.AuthorizationFailure( | ||||
|                         _("Cannot find endpoint or token for request")) | ||||
|  | ||||
|         old_token_endpoint = (token, endpoint) | ||||
|         kwargs.setdefault("headers", {})["X-Auth-Token"] = token | ||||
|         self.cached_token = token | ||||
|         client.cached_endpoint = endpoint | ||||
|         # Perform the request once. If we get Unauthorized, then it | ||||
|         # might be because the auth token expired, so try to | ||||
|         # re-authenticate and try again. If it still fails, bail. | ||||
|         try: | ||||
|             return self.request( | ||||
|                 method, self.concat_url(endpoint, url), **kwargs) | ||||
|         except exceptions.Unauthorized as unauth_ex: | ||||
|             if just_authenticated: | ||||
|                 raise | ||||
|             self.cached_token = None | ||||
|             client.cached_endpoint = None | ||||
|             if self.auth_plugin.opts.get('token'): | ||||
|                 self.auth_plugin.opts['token'] = None | ||||
|             if self.auth_plugin.opts.get('endpoint'): | ||||
|                 self.auth_plugin.opts['endpoint'] = None | ||||
|             self.authenticate() | ||||
|             try: | ||||
|                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||
|                     **filter_args) | ||||
|             except exceptions.EndpointException: | ||||
|                 raise unauth_ex | ||||
|             if (not (token and endpoint) or | ||||
|                     old_token_endpoint == (token, endpoint)): | ||||
|                 raise unauth_ex | ||||
|             self.cached_token = token | ||||
|             client.cached_endpoint = endpoint | ||||
|             kwargs["headers"]["X-Auth-Token"] = token | ||||
|             return self.request( | ||||
|                 method, self.concat_url(endpoint, url), **kwargs) | ||||
|  | ||||
|     def add_client(self, base_client_instance): | ||||
|         """Add a new instance of :class:`BaseClient` descendant. | ||||
|  | ||||
|         `self` will store a reference to `base_client_instance`. | ||||
|  | ||||
|         Example: | ||||
|  | ||||
|         >>> def test_clients(): | ||||
|         ...     from keystonauth.auth import keystone | ||||
|         ...     from openstack.common.apiclient import client | ||||
|         ...     auth = keystone.KeystoneAuthPlugin( | ||||
|         ...         username="user", password="pass", tenant_name="tenant", | ||||
|         ...         auth_url="http://auth:5000/v2.0") | ||||
|         ...     openstack_client = client.HTTPClient(auth) | ||||
|         ...     # create nova client | ||||
|         ...     from novaclient.v1_1 import client | ||||
|         ...     client.Client(openstack_client) | ||||
|         ...     # create keystone client | ||||
|         ...     from keystonauth.v2_0 import client | ||||
|         ...     client.Client(openstack_client) | ||||
|         ...     # use them | ||||
|         ...     openstack_client.identity.tenants.list() | ||||
|         ...     openstack_client.compute.servers.list() | ||||
|         """ | ||||
|         service_type = base_client_instance.service_type | ||||
|         if service_type and not hasattr(self, service_type): | ||||
|             setattr(self, service_type, base_client_instance) | ||||
|  | ||||
|     def authenticate(self): | ||||
|         self.auth_plugin.authenticate(self) | ||||
|         # Store the authentication results in the keyring for later requests | ||||
|         if self.keyring_saver: | ||||
|             self.keyring_saver.save(self) | ||||
|  | ||||
|  | ||||
| class BaseClient(object): | ||||
|     """Top-level object to access the OpenStack API. | ||||
|  | ||||
|     This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` | ||||
|     will handle a bunch of issues such as authentication. | ||||
|     """ | ||||
|  | ||||
|     service_type = None | ||||
|     endpoint_type = None  # "publicURL" will be used | ||||
|     cached_endpoint = None | ||||
|  | ||||
|     def __init__(self, http_client, extensions=None): | ||||
|         self.http_client = http_client | ||||
|         http_client.add_client(self) | ||||
|  | ||||
|         # Add in any extensions... | ||||
|         if extensions: | ||||
|             for extension in extensions: | ||||
|                 if extension.manager_class: | ||||
|                     setattr(self, extension.name, | ||||
|                             extension.manager_class(self)) | ||||
|  | ||||
|     def client_request(self, method, url, **kwargs): | ||||
|         return self.http_client.client_request( | ||||
|             self, method, url, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def last_request_id(self): | ||||
|         return self.http_client.last_request_id | ||||
|  | ||||
|     def head(self, url, **kwargs): | ||||
|         return self.client_request("HEAD", url, **kwargs) | ||||
|  | ||||
|     def get(self, url, **kwargs): | ||||
|         return self.client_request("GET", url, **kwargs) | ||||
|  | ||||
|     def post(self, url, **kwargs): | ||||
|         return self.client_request("POST", url, **kwargs) | ||||
|  | ||||
|     def put(self, url, **kwargs): | ||||
|         return self.client_request("PUT", url, **kwargs) | ||||
|  | ||||
|     def delete(self, url, **kwargs): | ||||
|         return self.client_request("DELETE", url, **kwargs) | ||||
|  | ||||
|     def patch(self, url, **kwargs): | ||||
|         return self.client_request("PATCH", url, **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_class(api_name, version, version_map): | ||||
|         """Returns the client class for the requested API version | ||||
|  | ||||
|         :param api_name: the name of the API, e.g. 'compute', 'image', etc | ||||
|         :param version: the requested API version | ||||
|         :param version_map: a dict of client classes keyed by version | ||||
|         :rtype: a client class for the requested API version | ||||
|         """ | ||||
|         try: | ||||
|             client_path = version_map[str(version)] | ||||
|         except (KeyError, ValueError): | ||||
|             msg = _("Invalid %(api_name)s client version '%(version)s'. " | ||||
|                     "Must be one of: %(version_map)s") % { | ||||
|                         'api_name': api_name, | ||||
|                         'version': version, | ||||
|                         'version_map': ', '.join(version_map.keys())} | ||||
|             raise exceptions.UnsupportedVersion(msg) | ||||
|  | ||||
|         return importutils.import_class(client_path) | ||||
| @@ -1,177 +0,0 @@ | ||||
| # Copyright 2013 OpenStack Foundation | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| """ | ||||
| A fake server that "responds" to API methods with pre-canned responses. | ||||
|  | ||||
| All of these responses come from the spec, so if for some reason the spec's | ||||
| wrong the tests might raise AssertionError. I've indicated in comments the | ||||
| places where actual behavior differs from the spec. | ||||
| """ | ||||
|  | ||||
| # W0102: Dangerous default value %s as argument | ||||
| # pylint: disable=W0102 | ||||
|  | ||||
| import json | ||||
|  | ||||
| import requests | ||||
| import six | ||||
| from six.moves.urllib import parse | ||||
|  | ||||
| from keystoneauth.openstack.common.apiclient import client | ||||
|  | ||||
|  | ||||
| def assert_has_keys(dct, required=None, optional=None): | ||||
|     required = required or [] | ||||
|     optional = optional or [] | ||||
|     for k in required: | ||||
|         try: | ||||
|             assert k in dct | ||||
|         except AssertionError: | ||||
|             extra_keys = set(dct.keys()).difference(set(required + optional)) | ||||
|             raise AssertionError("found unexpected keys: %s" % | ||||
|                                  list(extra_keys)) | ||||
|  | ||||
|  | ||||
| class TestResponse(requests.Response): | ||||
|     """Wrap requests.Response and provide a convenient initialization. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, data): | ||||
|         super(TestResponse, self).__init__() | ||||
|         self._content_consumed = True | ||||
|         if isinstance(data, dict): | ||||
|             self.status_code = data.get('status_code', 200) | ||||
|             # Fake the text attribute to streamline Response creation | ||||
|             text = data.get('text', "") | ||||
|             if isinstance(text, (dict, list)): | ||||
|                 self._content = json.dumps(text) | ||||
|                 default_headers = { | ||||
|                     "Content-Type": "application/json", | ||||
|                 } | ||||
|             else: | ||||
|                 self._content = text | ||||
|                 default_headers = {} | ||||
|             if six.PY3 and isinstance(self._content, six.string_types): | ||||
|                 self._content = self._content.encode('utf-8', 'strict') | ||||
|             self.headers = data.get('headers') or default_headers | ||||
|         else: | ||||
|             self.status_code = data | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return (self.status_code == other.status_code and | ||||
|                 self.headers == other.headers and | ||||
|                 self._content == other._content) | ||||
|  | ||||
|  | ||||
| class FakeHTTPClient(client.HTTPClient): | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self.callstack = [] | ||||
|         self.fixtures = kwargs.pop("fixtures", None) or {} | ||||
|         if not args and "auth_plugin" not in kwargs: | ||||
|             args = (None, ) | ||||
|         super(FakeHTTPClient, self).__init__(*args, **kwargs) | ||||
|  | ||||
|     def assert_called(self, method, url, body=None, pos=-1): | ||||
|         """Assert than an API method was just called. | ||||
|         """ | ||||
|         expected = (method, url) | ||||
|         called = self.callstack[pos][0:2] | ||||
|         assert self.callstack, \ | ||||
|             "Expected %s %s but no calls were made." % expected | ||||
|  | ||||
|         assert expected == called, 'Expected %s %s; got %s %s' % \ | ||||
|             (expected + called) | ||||
|  | ||||
|         if body is not None: | ||||
|             if self.callstack[pos][3] != body: | ||||
|                 raise AssertionError('%r != %r' % | ||||
|                                      (self.callstack[pos][3], body)) | ||||
|  | ||||
|     def assert_called_anytime(self, method, url, body=None): | ||||
|         """Assert than an API method was called anytime in the test. | ||||
|         """ | ||||
|         expected = (method, url) | ||||
|  | ||||
|         assert self.callstack, \ | ||||
|             "Expected %s %s but no calls were made." % expected | ||||
|  | ||||
|         found = False | ||||
|         entry = None | ||||
|         for entry in self.callstack: | ||||
|             if expected == entry[0:2]: | ||||
|                 found = True | ||||
|                 break | ||||
|  | ||||
|         assert found, 'Expected %s %s; got %s' % \ | ||||
|             (method, url, self.callstack) | ||||
|         if body is not None: | ||||
|             assert entry[3] == body, "%s != %s" % (entry[3], body) | ||||
|  | ||||
|         self.callstack = [] | ||||
|  | ||||
|     def clear_callstack(self): | ||||
|         self.callstack = [] | ||||
|  | ||||
|     def authenticate(self): | ||||
|         pass | ||||
|  | ||||
|     def client_request(self, client, method, url, **kwargs): | ||||
|         # Check that certain things are called correctly | ||||
|         if method in ["GET", "DELETE"]: | ||||
|             assert "json" not in kwargs | ||||
|  | ||||
|         # Note the call | ||||
|         self.callstack.append( | ||||
|             (method, | ||||
|              url, | ||||
|              kwargs.get("headers") or {}, | ||||
|              kwargs.get("json") or kwargs.get("data"))) | ||||
|         try: | ||||
|             fixture = self.fixtures[url][method] | ||||
|         except KeyError: | ||||
|             pass | ||||
|         else: | ||||
|             return TestResponse({"headers": fixture[0], | ||||
|                                  "text": fixture[1]}) | ||||
|  | ||||
|         # Call the method | ||||
|         args = parse.parse_qsl(parse.urlparse(url)[4]) | ||||
|         kwargs.update(args) | ||||
|         munged_url = url.rsplit('?', 1)[0] | ||||
|         munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') | ||||
|         munged_url = munged_url.replace('-', '_') | ||||
|  | ||||
|         callback = "%s_%s" % (method.lower(), munged_url) | ||||
|  | ||||
|         if not hasattr(self, callback): | ||||
|             raise AssertionError('Called unknown API method: %s %s, ' | ||||
|                                  'expected fakes method name: %s' % | ||||
|                                  (method, url, callback)) | ||||
|  | ||||
|         resp = getattr(self, callback)(**kwargs) | ||||
|         if len(resp) == 3: | ||||
|             status, headers, body = resp | ||||
|         else: | ||||
|             status, body = resp | ||||
|             headers = {} | ||||
|         self.last_request_id = headers.get('x-openstack-request-id', | ||||
|                                            'req-test') | ||||
|         return TestResponse({ | ||||
|             "status_code": status, | ||||
|             "text": body, | ||||
|             "headers": headers, | ||||
|         }) | ||||
| @@ -1,87 +0,0 @@ | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| from oslo.utils import encodeutils | ||||
| import six | ||||
|  | ||||
| from keystoneauth.openstack.common._i18n import _ | ||||
| from keystoneauth.openstack.common.apiclient import exceptions | ||||
| from keystoneauth.openstack.common import uuidutils | ||||
|  | ||||
|  | ||||
| def find_resource(manager, name_or_id, **find_args): | ||||
|     """Look for resource in a given manager. | ||||
|  | ||||
|     Used as a helper for the _find_* methods. | ||||
|     Example: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         def _find_hypervisor(cs, hypervisor): | ||||
|             #Get a hypervisor by name or ID. | ||||
|             return cliutils.find_resource(cs.hypervisors, hypervisor) | ||||
|     """ | ||||
|     # first try to get entity as integer id | ||||
|     try: | ||||
|         return manager.get(int(name_or_id)) | ||||
|     except (TypeError, ValueError, exceptions.NotFound): | ||||
|         pass | ||||
|  | ||||
|     # now try to get entity as uuid | ||||
|     try: | ||||
|         if six.PY2: | ||||
|             tmp_id = encodeutils.safe_encode(name_or_id) | ||||
|         else: | ||||
|             tmp_id = encodeutils.safe_decode(name_or_id) | ||||
|  | ||||
|         if uuidutils.is_uuid_like(tmp_id): | ||||
|             return manager.get(tmp_id) | ||||
|     except (TypeError, ValueError, exceptions.NotFound): | ||||
|         pass | ||||
|  | ||||
|     # for str id which is not uuid | ||||
|     if getattr(manager, 'is_alphanum_id_allowed', False): | ||||
|         try: | ||||
|             return manager.get(name_or_id) | ||||
|         except exceptions.NotFound: | ||||
|             pass | ||||
|  | ||||
|     try: | ||||
|         try: | ||||
|             return manager.find(human_id=name_or_id, **find_args) | ||||
|         except exceptions.NotFound: | ||||
|             pass | ||||
|  | ||||
|         # finally try to find entity by name | ||||
|         try: | ||||
|             resource = getattr(manager, 'resource_class', None) | ||||
|             name_attr = resource.NAME_ATTR if resource else 'name' | ||||
|             kwargs = {name_attr: name_or_id} | ||||
|             kwargs.update(find_args) | ||||
|             return manager.find(**kwargs) | ||||
|         except exceptions.NotFound: | ||||
|             msg = _("No %(name)s with a name or " | ||||
|                     "ID of '%(name_or_id)s' exists.") % \ | ||||
|                 { | ||||
|                     "name": manager.resource_class.__name__.lower(), | ||||
|                     "name_or_id": name_or_id | ||||
|                 } | ||||
|             raise exceptions.CommandError(msg) | ||||
|     except exceptions.NoUniqueMatch: | ||||
|         msg = _("Multiple %(name)s matches found for " | ||||
|                 "'%(name_or_id)s', use an ID to be more specific.") % \ | ||||
|             { | ||||
|                 "name": manager.resource_class.__name__.lower(), | ||||
|                 "name_or_id": name_or_id | ||||
|             } | ||||
|         raise exceptions.CommandError(msg) | ||||
| @@ -1,90 +0,0 @@ | ||||
| # Copyright 2010 United States Government as represented by the | ||||
| # Administrator of the National Aeronautics and Space Administration. | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| """Super simple fake memcache client.""" | ||||
|  | ||||
| from oslo.config import cfg | ||||
| from oslo.utils import timeutils | ||||
|  | ||||
| memcache_opts = [ | ||||
|     cfg.ListOpt('memcached_servers', | ||||
|                 help='Memcached servers or None for in process cache.'), | ||||
| ] | ||||
|  | ||||
| CONF = cfg.CONF | ||||
| CONF.register_opts(memcache_opts) | ||||
|  | ||||
|  | ||||
| def get_client(memcached_servers=None): | ||||
|     client_cls = Client | ||||
|  | ||||
|     if not memcached_servers: | ||||
|         memcached_servers = CONF.memcached_servers | ||||
|     if memcached_servers: | ||||
|         import memcache | ||||
|         client_cls = memcache.Client | ||||
|  | ||||
|     return client_cls(memcached_servers, debug=0) | ||||
|  | ||||
|  | ||||
| class Client(object): | ||||
|     """Replicates a tiny subset of memcached client interface.""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Ignores the passed in args.""" | ||||
|         self.cache = {} | ||||
|  | ||||
|     def get(self, key): | ||||
|         """Retrieves the value for a key or None. | ||||
|  | ||||
|         This expunges expired keys during each get. | ||||
|         """ | ||||
|  | ||||
|         now = timeutils.utcnow_ts() | ||||
|         for k in list(self.cache): | ||||
|             (timeout, _value) = self.cache[k] | ||||
|             if timeout and now >= timeout: | ||||
|                 del self.cache[k] | ||||
|  | ||||
|         return self.cache.get(key, (0, None))[1] | ||||
|  | ||||
|     def set(self, key, value, time=0, min_compress_len=0): | ||||
|         """Sets the value for a key.""" | ||||
|         timeout = 0 | ||||
|         if time != 0: | ||||
|             timeout = timeutils.utcnow_ts() + time | ||||
|         self.cache[key] = (timeout, value) | ||||
|         return True | ||||
|  | ||||
|     def add(self, key, value, time=0, min_compress_len=0): | ||||
|         """Sets the value for a key if it doesn't exist.""" | ||||
|         if self.get(key) is not None: | ||||
|             return False | ||||
|         return self.set(key, value, time, min_compress_len) | ||||
|  | ||||
|     def incr(self, key, delta=1): | ||||
|         """Increments the value for a key.""" | ||||
|         value = self.get(key) | ||||
|         if value is None: | ||||
|             return None | ||||
|         new_value = int(value) + delta | ||||
|         self.cache[key] = (self.cache[key][0], str(new_value)) | ||||
|         return new_value | ||||
|  | ||||
|     def delete(self, key, time=0): | ||||
|         """Deletes the value associated with a key.""" | ||||
|         if key in self.cache: | ||||
|             del self.cache[key] | ||||
| @@ -1,37 +0,0 @@ | ||||
| # Copyright (c) 2012 Intel Corporation. | ||||
| # All Rights Reserved. | ||||
| # | ||||
| #    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. | ||||
|  | ||||
| """ | ||||
| UUID related utilities and helper functions. | ||||
| """ | ||||
|  | ||||
| import uuid | ||||
|  | ||||
|  | ||||
| def generate_uuid(): | ||||
|     return str(uuid.uuid4()) | ||||
|  | ||||
|  | ||||
| def is_uuid_like(val): | ||||
|     """Returns validation of a value as a UUID. | ||||
|  | ||||
|     For our purposes, a UUID is a canonical form string: | ||||
|     aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa | ||||
|  | ||||
|     """ | ||||
|     try: | ||||
|         return str(uuid.UUID(val)) == val | ||||
|     except (TypeError, ValueError, AttributeError): | ||||
|         return False | ||||
| @@ -420,11 +420,16 @@ class Session(object): | ||||
|                 raise exceptions.SSLError(msg) | ||||
|             except requests.exceptions.Timeout: | ||||
|                 msg = _('Request to %s timed out') % url | ||||
|                 raise exceptions.RequestTimeout(msg) | ||||
|                 raise exceptions.ConnectTimeout(msg) | ||||
|             except requests.exceptions.ConnectionError: | ||||
|                 msg = _('Unable to establish connection to %s') % url | ||||
|                 raise exceptions.ConnectionRefused(msg) | ||||
|         except (exceptions.RequestTimeout, exceptions.ConnectionRefused) as e: | ||||
|                 raise exceptions.ConnectFailure(msg) | ||||
|             except requests.exception.RequestException as e: | ||||
|                 msg = _('Unexpected exception for %(url)s: %(error)s') % { | ||||
|                     'url': url, 'error': e} | ||||
|                 raise exceptions.UnknownConnectionError(msg, e) | ||||
|  | ||||
|         except exceptions.RetriableConnectionFailure as e: | ||||
|             if connect_retries <= 0: | ||||
|                 raise | ||||
|  | ||||
|   | ||||
| @@ -185,17 +185,13 @@ class SessionTests(utils.TestCase): | ||||
|         self.assertIn(path_to_certs, self.logger.output) | ||||
|  | ||||
|     def test_connect_retries(self): | ||||
|  | ||||
|         def _timeout_error(request, context): | ||||
|             raise requests.exceptions.Timeout() | ||||
|  | ||||
|         self.stub_url('GET', text=_timeout_error) | ||||
|         self.stub_url('GET', exc=requests.exceptions.Timeout()) | ||||
|  | ||||
|         session = client_session.Session() | ||||
|         retries = 3 | ||||
|  | ||||
|         with mock.patch('time.sleep') as m: | ||||
|             self.assertRaises(exceptions.RequestTimeout, | ||||
|             self.assertRaises(exceptions.ConnectTimeout, | ||||
|                               session.get, | ||||
|                               self.TEST_URL, connect_retries=retries) | ||||
|  | ||||
| @@ -223,10 +219,7 @@ class SessionTests(utils.TestCase): | ||||
|     def test_ssl_error_message(self): | ||||
|         error = uuid.uuid4().hex | ||||
|  | ||||
|         def _ssl_error(request, context): | ||||
|             raise requests.exceptions.SSLError(error) | ||||
|  | ||||
|         self.stub_url('GET', text=_ssl_error) | ||||
|         self.stub_url('GET', exc=requests.exceptions.SSLError(error)) | ||||
|         session = client_session.Session() | ||||
|  | ||||
|         # The exception should contain the URL and details about the SSL error | ||||
| @@ -796,13 +789,10 @@ class AdapterTest(utils.TestCase): | ||||
|         sess = client_session.Session() | ||||
|         adpt = adapter.Adapter(sess, connect_retries=retries) | ||||
|  | ||||
|         def _refused_error(request, context): | ||||
|             raise requests.exceptions.ConnectionError() | ||||
|  | ||||
|         self.stub_url('GET', text=_refused_error) | ||||
|         self.stub_url('GET', exc=requests.exceptions.ConnectionError()) | ||||
|  | ||||
|         with mock.patch('time.sleep') as m: | ||||
|             self.assertRaises(exceptions.ConnectionRefused, | ||||
|             self.assertRaises(exceptions.ConnectionError, | ||||
|                               adpt.get, self.TEST_URL) | ||||
|             self.assertEqual(retries, m.call_count) | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| [DEFAULT] | ||||
|  | ||||
| # The list of modules to copy from oslo-incubator | ||||
| module=apiclient | ||||
| module=install_venv_common | ||||
|  | ||||
| # The base module to hold the copy of openstack.common | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jamie Lennox
					Jamie Lennox