
As part of the first community-wide goal, teams were asked to remove the openstack/common package of their projects if one existed. This was a byproduct of the old oslo-incubator form of syncing common functionality. The package, apiclient, was moved to a top level location and cliutils was moved to the common module. There are no oslo specific libraries, the recommended solution is to move it in tree and maintain it there. Change-Id: Iee52004bd33c19d63133577ff466164b85fd6ca6
445 lines
12 KiB
Python
445 lines
12 KiB
Python
# 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 inspect
|
|
import sys
|
|
|
|
import six
|
|
|
|
|
|
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 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 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 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 = "%s (HTTP %s)" % (self.message, 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 contains all the classes that have http_status attribute.
|
|
_code_map = dict(
|
|
(getattr(obj, 'http_status', None), obj)
|
|
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
|
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
|
)
|
|
|
|
|
|
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[list(body.keys())[0]]
|
|
kwargs["message"] = error.get("message", None)
|
|
kwargs["details"] = error.get("details", None)
|
|
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)
|