We have a new version of types-requests. Change-Id: Ib071b1fd9057125ea7bf883711dbc65c49b25990 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
297 lines
9.1 KiB
Python
297 lines
9.1 KiB
Python
# 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.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import typing as ty
|
|
import warnings
|
|
|
|
import requests
|
|
from requests import exceptions as _rex
|
|
|
|
from openstack import warnings as os_warnings
|
|
|
|
if ty.TYPE_CHECKING:
|
|
from openstack import resource
|
|
|
|
|
|
class SDKException(Exception):
|
|
"""The base exception class for all exceptions this library raises."""
|
|
|
|
def __init__(self, message: str | None = None, extra_data: ty.Any = None):
|
|
self.message = self.__class__.__name__ if message is None else message
|
|
self.extra_data = extra_data
|
|
super().__init__(self.message)
|
|
|
|
|
|
class EndpointNotFound(SDKException):
|
|
"""A mismatch occurred between what the client and server expect."""
|
|
|
|
def __init__(self, message: str | None = None):
|
|
super().__init__(message)
|
|
|
|
|
|
class InvalidResponse(SDKException):
|
|
"""The response from the server is not valid for this request."""
|
|
|
|
def __init__(self, message: str | None = None):
|
|
super().__init__(message)
|
|
|
|
|
|
class InvalidRequest(SDKException):
|
|
"""The request to the server is not valid."""
|
|
|
|
def __init__(self, message: str | None = None):
|
|
super().__init__(message)
|
|
|
|
|
|
class HttpException(SDKException, _rex.HTTPError):
|
|
"""The base exception for all HTTP error responses."""
|
|
|
|
source: str
|
|
status_code: int | None
|
|
|
|
def __init__(
|
|
self,
|
|
message: str | None = 'Error',
|
|
response: requests.Response | None = None,
|
|
http_status: int | None = None,
|
|
details: str | None = None,
|
|
request_id: str | None = None,
|
|
):
|
|
if http_status is not None:
|
|
warnings.warn(
|
|
"The 'http_status' parameter is unnecessary and will be "
|
|
"removed in a future release",
|
|
os_warnings.RemovedInSDK50Warning,
|
|
)
|
|
|
|
if request_id is not None:
|
|
warnings.warn(
|
|
"The 'request_id' parameter is unnecessary and will be "
|
|
"removed in a future release",
|
|
os_warnings.RemovedInSDK50Warning,
|
|
)
|
|
|
|
if not message:
|
|
if response is not None:
|
|
message = f"{self.__class__.__name__}: {response.status_code}"
|
|
else:
|
|
message = f"{self.__class__.__name__}: Unknown error"
|
|
status = (
|
|
response.status_code
|
|
if response is not None
|
|
else 'Unknown error'
|
|
)
|
|
message = f'{self.__class__.__name__}: {status}'
|
|
|
|
# Call directly rather than via super to control parameters
|
|
SDKException.__init__(self, message=message)
|
|
_rex.HTTPError.__init__(self, message, response=response)
|
|
|
|
if response is not None:
|
|
self.request_id = response.headers.get('x-openstack-request-id')
|
|
self.status_code = response.status_code
|
|
else:
|
|
self.request_id = request_id
|
|
self.status_code = http_status
|
|
self.details = details
|
|
self.url = (self.request and self.request.url) or None
|
|
self.method = (self.request and self.request.method) or None
|
|
self.source = "Server"
|
|
if self.status_code is not None and (400 <= self.status_code < 500):
|
|
self.source = "Client"
|
|
|
|
def __str__(self) -> str:
|
|
# 'Error' is the default value for self.message. If self.message isn't
|
|
# 'Error', then someone has set a more informative error message
|
|
# and we should use it. If it is 'Error', then we should construct a
|
|
# better message from the information we do have.
|
|
if not self.url or self.message == 'Error':
|
|
return self.message
|
|
if self.url:
|
|
remote_error = f"{self.source} Error for url: {self.url}"
|
|
if self.details:
|
|
remote_error += ', '
|
|
if self.details:
|
|
remote_error += str(self.details)
|
|
|
|
return f"{super().__str__()}: {remote_error}"
|
|
|
|
|
|
class BadRequestException(HttpException):
|
|
"""HTTP 400 Bad Request."""
|
|
|
|
|
|
class NotFoundException(HttpException):
|
|
"""HTTP 404 Not Found."""
|
|
|
|
|
|
class ForbiddenException(HttpException):
|
|
"""HTTP 403 Forbidden Request."""
|
|
|
|
|
|
class ConflictException(HttpException):
|
|
"""HTTP 409 Conflict."""
|
|
|
|
|
|
class PreconditionFailedException(HttpException):
|
|
"""HTTP 412 Precondition Failed."""
|
|
|
|
|
|
class MethodNotSupported(SDKException):
|
|
"""The resource does not support this operation type."""
|
|
|
|
def __init__(
|
|
self,
|
|
resource: ty.Union['resource.Resource', type['resource.Resource']],
|
|
method: str,
|
|
):
|
|
# This needs to work with both classes and instances.
|
|
try:
|
|
name = resource.__name__
|
|
except AttributeError:
|
|
name = resource.__class__.__name__
|
|
|
|
message = (
|
|
f'The {method} method is not supported for '
|
|
f'{resource.__module__}.{name}'
|
|
)
|
|
super().__init__(message=message)
|
|
|
|
|
|
class DuplicateResource(SDKException):
|
|
"""More than one resource exists with that name."""
|
|
|
|
|
|
class ResourceTimeout(SDKException):
|
|
"""Timeout waiting for resource."""
|
|
|
|
|
|
class ResourceFailure(SDKException):
|
|
"""General resource failure."""
|
|
|
|
|
|
class InvalidResourceQuery(SDKException):
|
|
"""Invalid query params for resource."""
|
|
|
|
|
|
def _extract_message(obj: ty.Any) -> str | None:
|
|
if isinstance(obj, dict):
|
|
# Most of services: compute, network
|
|
if obj.get('message'):
|
|
return str(obj['message'])
|
|
# Ironic starting with Stein
|
|
elif obj.get('faultstring'):
|
|
return str(obj['faultstring'])
|
|
elif isinstance(obj, str):
|
|
# Ironic before Stein has double JSON encoding, nobody remembers why.
|
|
try:
|
|
obj = json.loads(obj)
|
|
except Exception: # noqa: S110
|
|
# This is best effort. Ignore any errors.
|
|
pass
|
|
else:
|
|
return _extract_message(obj)
|
|
return None
|
|
|
|
|
|
def raise_from_response(
|
|
response: requests.Response,
|
|
error_message: str | None = None,
|
|
) -> None:
|
|
"""Raise an instance of an HTTPException based on keystoneauth response."""
|
|
if response.status_code < 400:
|
|
return
|
|
|
|
cls: type[SDKException]
|
|
if response.status_code == 400:
|
|
cls = BadRequestException
|
|
elif response.status_code == 403:
|
|
cls = ForbiddenException
|
|
elif response.status_code == 404:
|
|
cls = NotFoundException
|
|
elif response.status_code == 409:
|
|
cls = ConflictException
|
|
elif response.status_code == 412:
|
|
cls = PreconditionFailedException
|
|
else:
|
|
cls = HttpException
|
|
|
|
details = None
|
|
content_type = response.headers.get('content-type', '')
|
|
if response.content and 'application/json' in content_type:
|
|
# Iterate over the nested objects to retrieve "message" attribute.
|
|
# TODO(shade) Add exception handling for times when the content type
|
|
# is lying.
|
|
|
|
try:
|
|
content = response.json()
|
|
messages = [_extract_message(obj) for obj in content.values()]
|
|
if not any(messages):
|
|
# Exception dict may be the root dict in projects that use WSME
|
|
messages = [_extract_message(content)]
|
|
# Join all of the messages together nicely and filter out any
|
|
# objects that don't have a "message" attr.
|
|
details = '\n'.join(msg for msg in messages if msg)
|
|
except Exception:
|
|
details = response.text
|
|
elif response.content and 'text/html' in content_type:
|
|
messages = []
|
|
for line in response.text.splitlines():
|
|
message = re.sub(r'<.+?>', '', line.strip())
|
|
if message not in messages:
|
|
messages.append(message)
|
|
|
|
# Return joined string separated by colons.
|
|
details = ': '.join(msg for msg in messages if msg)
|
|
|
|
if not details:
|
|
details = response.reason if response.reason else response.text
|
|
|
|
raise cls(
|
|
message=error_message,
|
|
response=response,
|
|
details=details,
|
|
)
|
|
|
|
|
|
class ConfigException(SDKException):
|
|
"""Something went wrong with parsing your OpenStack Config."""
|
|
|
|
|
|
class NotSupported(SDKException):
|
|
"""Request cannot be performed by any supported API version."""
|
|
|
|
|
|
class ValidationException(SDKException):
|
|
"""Validation failed for resource."""
|
|
|
|
|
|
class ServiceDisabledException(ConfigException):
|
|
"""This service is disabled for reasons."""
|
|
|
|
|
|
class ServiceDiscoveryException(SDKException):
|
|
"""The service cannot be discovered."""
|
|
|
|
|
|
# Backwards compatibility
|
|
OpenStackCloudException = SDKException
|
|
ResourceNotFound = NotFoundException
|