diff --git a/keystoneclient/apiclient/__init__.py b/keystoneclient/apiclient/__init__.py new file mode 100644 index 000000000..d5d002224 --- /dev/null +++ b/keystoneclient/apiclient/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. diff --git a/keystoneclient/apiclient/exceptions.py b/keystoneclient/apiclient/exceptions.py new file mode 100644 index 000000000..852456f5b --- /dev/null +++ b/keystoneclient/apiclient/exceptions.py @@ -0,0 +1,442 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 Nebula, Inc. +# Copyright 2013 Alessio Ababilov +# 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. + +""" +Exception definitions. +""" + +import itertools + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises. + """ + pass + + +class MissingArgs(ClientException): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = "Missing argument(s): %s" % ", ".join(missing) + super(MissingArgs, self).__init__(msg) + + +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 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 a 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 EmptyCatalog(EndpointNotFound): + """The service catalog is empty.""" + 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): + """The base exception class for all HTTP exceptions. + """ + http_status = 0 + message = "HTTP Error" + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, http_status=None): + self.http_status = http_status or self.http_status + self.message = message or self.message + self.details = details + self.request_id = request_id + self.response = response + self.url = url + self.method = method + formatted_string = "%(message)s (HTTP %(status)s)" % { + "message": self.message, "status": self.http_status} + if request_id: + formatted_string += " (Request-ID: %s)" % request_id + super(HTTPError, self).__init__(formatted_string) + + +class HTTPClientError(HTTPError): + """Client-side HTTP error. + + Exception for cases in which the client seems to have erred. + """ + message = "HTTP Client Error" + + +class HTTPServerError(HTTPError): + """Server-side HTTP error. + + Exception for cases in which the server is aware that it has + erred or is incapable of performing the request. + """ + message = "HTTP Server Error" + + +class BadRequest(HTTPClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + http_status = 400 + message = "Bad Request" + + +class Unauthorized(HTTPClientError): + """HTTP 401 - Unauthorized. + + Similar to 403 Forbidden, but specifically for use when authentication + is required and has failed or has not yet been provided. + """ + http_status = 401 + message = "Unauthorized" + + +class PaymentRequired(HTTPClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + http_status = 402 + message = "Payment Required" + + +class Forbidden(HTTPClientError): + """HTTP 403 - Forbidden. + + The request was a valid request, but the server is refusing to respond + to it. + """ + http_status = 403 + message = "Forbidden" + + +class NotFound(HTTPClientError): + """HTTP 404 - Not Found. + + The requested resource could not be found but may be available again + in the future. + """ + http_status = 404 + message = "Not Found" + + +class MethodNotAllowed(HTTPClientError): + """HTTP 405 - Method Not Allowed. + + A request was made of a resource using a request method not supported + by that resource. + """ + http_status = 405 + message = "Method Not Allowed" + + +class NotAcceptable(HTTPClientError): + """HTTP 406 - Not Acceptable. + + The requested resource is only capable of generating content not + acceptable according to the Accept headers sent in the request. + """ + http_status = 406 + message = "Not Acceptable" + + +class ProxyAuthenticationRequired(HTTPClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + http_status = 407 + message = "Proxy Authentication Required" + + +class RequestTimeout(HTTPClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + http_status = 408 + message = "Request Timeout" + + +class Conflict(HTTPClientError): + """HTTP 409 - Conflict. + + Indicates that the request could not be processed because of conflict + in the request, such as an edit conflict. + """ + http_status = 409 + message = "Conflict" + + +class Gone(HTTPClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + http_status = 410 + message = "Gone" + + +class LengthRequired(HTTPClientError): + """HTTP 411 - Length Required. + + The request did not specify the length of its content, which is + required by the requested resource. + """ + http_status = 411 + message = "Length Required" + + +class PreconditionFailed(HTTPClientError): + """HTTP 412 - Precondition Failed. + + The server does not meet one of the preconditions that the requester + put on the request. + """ + http_status = 412 + message = "Precondition Failed" + + +class RequestEntityTooLarge(HTTPClientError): + """HTTP 413 - Request Entity Too Large. + + The request is larger than the server is willing or able to process. + """ + http_status = 413 + message = "Request Entity Too Large" + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RequestEntityTooLarge, self).__init__(*args, **kwargs) + + +class RequestUriTooLong(HTTPClientError): + """HTTP 414 - Request-URI Too Long. + + The URI provided was too long for the server to process. + """ + http_status = 414 + message = "Request-URI Too Long" + + +class UnsupportedMediaType(HTTPClientError): + """HTTP 415 - Unsupported Media Type. + + The request entity has a media type which the server or resource does + not support. + """ + http_status = 415 + message = "Unsupported Media Type" + + +class RequestedRangeNotSatisfiable(HTTPClientError): + """HTTP 416 - Requested Range Not Satisfiable. + + The client has asked for a portion of the file, but the server cannot + supply that portion. + """ + http_status = 416 + message = "Requested Range Not Satisfiable" + + +class ExpectationFailed(HTTPClientError): + """HTTP 417 - Expectation Failed. + + The server cannot meet the requirements of the Expect request-header field. + """ + http_status = 417 + message = "Expectation Failed" + + +class UnprocessableEntity(HTTPClientError): + """HTTP 422 - Unprocessable Entity. + + The request was well-formed but was unable to be followed due to semantic + errors. + """ + http_status = 422 + message = "Unprocessable Entity" + + +class InternalServerError(HTTPServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + http_status = 500 + message = "Internal Server Error" + + +# NotImplemented is a python keyword. +class HTTPNotImplemented(HTTPServerError): + """HTTP 501 - Not Implemented. + + The server either does not recognize the request method, or it lacks + the ability to fulfill the request. + """ + http_status = 501 + message = "Not Implemented" + + +class BadGateway(HTTPServerError): + """HTTP 502 - Bad Gateway. + + The server was acting as a gateway or proxy and received an invalid + response from the upstream server. + """ + http_status = 502 + message = "Bad Gateway" + + +class ServiceUnavailable(HTTPServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + http_status = 503 + message = "Service Unavailable" + + +class GatewayTimeout(HTTPServerError): + """HTTP 504 - Gateway Timeout. + + The server was acting as a gateway or proxy and did not receive a timely + response from the upstream server. + """ + http_status = 504 + message = "Gateway Timeout" + + +class HTTPVersionNotSupported(HTTPServerError): + """HTTP 505 - HTTPVersion Not Supported. + + The server does not support the HTTP protocol version used in the request. + """ + http_status = 505 + message = "HTTP Version Not Supported" + + +_code_map = dict( + (cls.http_status, cls) + for cls in itertools.chain(HTTPClientError.__subclasses__(), + HTTPServerError.__subclasses__())) + + +def from_response(response, method, url): + """Returns an instance of :class:`HTTPError` or subclass based on response. + + :param response: instance of `requests.Response` class + :param method: HTTP method used for request + :param url: URL used for request + """ + kwargs = { + "http_status": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": response.headers.get("x-compute-request-id"), + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if hasattr(body, "keys"): + error = body[body.keys()[0]] + kwargs["message"] = error.get("message") + kwargs["details"] = error.get("details") + elif content_type.startswith("text/"): + kwargs["details"] = response.text + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HTTPServerError + elif 400 <= response.status_code < 500: + cls = HTTPClientError + else: + cls = HTTPError + return cls(**kwargs) diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index 5990a9970..2bd6c37c4 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -18,162 +18,5 @@ Exception definitions. """ -from keystoneclient.openstack.common import jsonutils - - -class CommandError(Exception): - pass - - -class ValidationError(Exception): - pass - - -class AuthorizationFailure(Exception): - pass - - -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token. - """ - pass - - -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class EmptyCatalog(Exception): - """The service catalog is empty.""" - pass - - -class NoUniqueMatch(Exception): - """Unable to find unique resource.""" - pass - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - - def __init__(self, code, message=None, details=None): - self.code = code - self.message = message or self.__class__.message - self.details = details - - def __str__(self): - return "%s (HTTP %s)" % (self.message, self.code) - - -class BadRequest(ClientException): - """HTTP 400 - Bad request: you sent some malformed data.""" - - http_status = 400 - message = "Bad request" - - -class Unauthorized(ClientException): - """HTTP 401 - Unauthorized: bad credentials.""" - - http_status = 401 - message = "Unauthorized" - - -class Forbidden(ClientException): - """HTTP 403 - Forbidden: your credentials do not allow access to this - resource. - """ - http_status = 403 - message = "Forbidden" - - -class NotFound(ClientException): - """HTTP 404 - Not found.""" - http_status = 404 - message = "Not found" - - -class MethodNotAllowed(ClientException): - """HTTP 405 - Method not allowed.""" - http_status = 405 - message = "Method not allowed" - - -class Conflict(ClientException): - """HTTP 409 - Conflict.""" - http_status = 409 - message = "Conflict" - - -class OverLimit(ClientException): - """HTTP 413 - Over limit: you're over the API limits for this time - period. - """ - http_status = 413 - message = "Over limit" - - -# NotImplemented is a python keyword. -class HTTPNotImplemented(ClientException): - """HTTP 501 - Not Implemented: the server does not support this - operation. - """ - http_status = 501 - message = "Not Implemented" - - -class ServiceUnavailable(ClientException): - """HTTP 503 - Service Unavailable: The server is currently unavailable.""" - http_status = 503 - message = "Service Unavailable" - - -# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() -# so we can do this: -# _code_map = dict((c.http_status, c) -# for c in ClientException.__subclasses__()) -# -# Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, - Unauthorized, - Forbidden, - NotFound, - MethodNotAllowed, - Conflict, - OverLimit, - HTTPNotImplemented, - ServiceUnavailable]) - - -def from_response(response, body=None): - """Return an instance of a ClientException or subclass - based on a requests response. - - Usage:: - - resp = requests.request(...) - if resp.status_code != 200: - raise exception_from_response(resp, resp.text) - """ - cls = _code_map.get(response.status_code, ClientException) - if body is None: - try: - body = jsonutils.loads(response.text) - except Exception: - body = response.text - - if body: - if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) - else: - # If we didn't get back a properly formed error message we - # probably couldn't communicate with Keystone at all. - message = "Unable to communicate with identity service: %s." % body - details = None - return cls(code=response.status_code, message=message, details=details) - else: - return cls(code=response.status_code) +#flake8: noqa +from keystoneclient.apiclient.exceptions import * diff --git a/keystoneclient/generic/client.py b/keystoneclient/generic/client.py index 4533de56a..7ad82219d 100644 --- a/keystoneclient/generic/client.py +++ b/keystoneclient/generic/client.py @@ -120,7 +120,7 @@ class Client(httpclient.HTTPClient): elif resp.status_code == 305: return self._check_keystone_versions(resp['location']) else: - raise exceptions.from_response(resp, resp.text) + raise exceptions.from_response(resp, "GET", url) except Exception as e: _logger.exception(e) @@ -178,7 +178,8 @@ class Client(httpclient.HTTPClient): elif resp.status_code == 305: return self._check_keystone_extensions(resp['location']) else: - raise exceptions.from_response(resp, resp.text) + raise exceptions.from_response( + resp, "GET", "%sextensions" % url) except Exception as e: _logger.exception(e) diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py index 9d0b77df5..46e2b8909 100644 --- a/keystoneclient/httpclient.py +++ b/keystoneclient/httpclient.py @@ -120,7 +120,7 @@ def request(url, method='GET', headers=None, original_ip=None, debug=False, if resp.status_code >= 400: logger.debug("Request returned failure status: %s", resp.status_code) - raise exceptions.from_response(resp) + raise exceptions.from_response(resp, method, url) return resp diff --git a/tests/apiclient/test_exceptions.py b/tests/apiclient/test_exceptions.py new file mode 100644 index 000000000..cfdf91909 --- /dev/null +++ b/tests/apiclient/test_exceptions.py @@ -0,0 +1,67 @@ +# Copyright 2012 OpenStack LLC. +# 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. + +from tests import utils + +from keystoneclient.apiclient import exceptions + + +class FakeResponse(object): + json_data = {} + + def __init__(self, **kwargs): + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + def json(self): + return self.json_data + + +class ExceptionsArgsTest(utils.TestCase): + + def assert_exception(self, ex_cls, method, url, status_code, json_data): + ex = exceptions.from_response( + FakeResponse(status_code=status_code, + headers={"Content-Type": "application/json"}, + json_data=json_data), + method, + url) + self.assertTrue(isinstance(ex, ex_cls)) + self.assertEqual(ex.message, json_data["error"]["message"]) + self.assertEqual(ex.details, json_data["error"]["details"]) + self.assertEqual(ex.method, method) + self.assertEqual(ex.url, url) + self.assertEqual(ex.http_status, status_code) + + def test_from_response_known(self): + method = "GET" + url = "/fake" + status_code = 400 + json_data = {"error": {"message": "fake message", + "details": "fake details"}} + self.assert_exception( + exceptions.BadRequest, method, url, status_code, json_data) + + def test_from_response_unknown(self): + method = "POST" + url = "/fake-unknown" + status_code = 499 + json_data = {"error": {"message": "fake unknown message", + "details": "fake unknown details"}} + self.assert_exception( + exceptions.HTTPClientError, method, url, status_code, json_data) + status_code = 600 + self.assert_exception( + exceptions.HTTPError, method, url, status_code, json_data) diff --git a/tests/test_http.py b/tests/test_http.py index 257052f9b..e5c7cbcee 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -107,7 +107,8 @@ class ClientTest(utils.TestCase): } fake_err_response = utils.TestResponse({ "status_code": 400, - "text": json.dumps(err_response) + "text": json.dumps(err_response), + "headers": {"Content-Type": "application/json"}, }) err_MOCK_REQUEST = mock.Mock(return_value=(fake_err_response))