Map KSA exception to SDK exceptions

The use of KSA is an implementation detail. Users should not have to
know about KSA. Catch all KSA exceptions and map them to SDK
exceptions.

Change-Id: Ib873c038358fb318ae46e8bc5bd3b4dfb683daaa
Closes-Bug: #1526516
This commit is contained in:
Everett Toews 2016-01-07 22:52:07 -06:00
parent 0c7a3430ac
commit e0ad907a36
10 changed files with 126 additions and 95 deletions

View File

@ -60,12 +60,9 @@ try to find it and if that fails, you would create it::
import logging
import sys
from keystoneauth1 import exceptions as ksa_exc
from keystoneauth1.loading import base as ksa_loader
import os_client_config
import six
from openstack import exceptions
from openstack import profile as _profile
from openstack import proxy
from openstack import session as _session
@ -250,19 +247,6 @@ class Connection(object):
to be authorized or the `auth_plugin` argument is missing,
etc.
"""
try:
headers = self.session.get_auth_headers()
except ksa_exc.AuthorizationFailure as ex:
raise exceptions.HttpException("Authentication Failure",
six.text_type(ex),
status_code=401)
except ksa_exc.MissingAuthPlugin as ex:
raise exceptions.HttpException("Bad Request",
six.text_type(ex),
status_code=400)
except Exception as ex:
raise exceptions.HttpException("Unknown exception",
six.text_type(ex),
status_code=500)
headers = self.session.get_auth_headers()
return headers.get('X-Auth-Token') if headers else None

View File

@ -21,9 +21,10 @@ import six
class SDKException(Exception):
"""The base exception class for all exceptions this library raises."""
def __init__(self, message=None):
def __init__(self, message=None, cause=None):
self.message = self.__class__.__name__ if message is None else message
super(Exception, self).__init__(self.message)
self.cause = cause
super(SDKException, self).__init__(self.message)
class InvalidResponse(SDKException):
@ -35,10 +36,17 @@ class InvalidResponse(SDKException):
class HttpException(SDKException):
def __init__(self, message, details=None, status_code=None):
super(HttpException, self).__init__(message)
def __init__(self, message=None, details=None, response=None,
request_id=None, url=None, method=None,
http_status=None, cause=None):
super(HttpException, self).__init__(message=message, cause=cause)
self.details = details
self.status_code = status_code
self.response = response
self.request_id = request_id
self.url = url
self.method = method
self.http_status = http_status
def __unicode__(self):
msg = self.__class__.__name__ + ": " + self.message
@ -58,9 +66,9 @@ class NotFoundException(HttpException):
class MethodNotSupported(SDKException):
"""The resource does not support this operation type."""
def __init__(self, cls, method):
msg = ('The %s method is not supported for %s.%s' %
(method, cls.__module__, cls.__name__))
super(Exception, self).__init__(msg)
message = ('The %s method is not supported for %s.%s' %
(method, cls.__module__, cls.__name__))
super(MethodNotSupported, self).__init__(message=message)
class DuplicateResource(SDKException):

View File

@ -10,7 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystoneauth1 import exceptions as ksa_exc
from openstack import exceptions
from openstack import resource
@ -124,14 +123,17 @@ class BaseProxy(object):
try:
rv = res.delete(self.session)
except ksa_exc.NotFound as exc:
except exceptions.NotFoundException as e:
if ignore_missing:
return None
else:
# Reraise with a more specific type and message
raise exceptions.ResourceNotFound(
"No %s found for %s" % (resource_type.__name__, value),
details=exc.details, status_code=exc.http_status)
message="No %s found for %s" %
(resource_type.__name__, value),
details=e.details, response=e.response,
request_id=e.request_id, url=e.url, method=e.method,
http_status=e.http_status, cause=e.cause)
return rv
@ -197,10 +199,13 @@ class BaseProxy(object):
try:
return res.get(self.session, args=args)
except ksa_exc.NotFound as exc:
except exceptions.NotFoundException as e:
raise exceptions.ResourceNotFound(
"No %s found for %s" % (resource_type.__name__, value),
details=exc.details, status_code=exc.http_status)
message="No %s found for %s" %
(resource_type.__name__, value),
details=e.details, response=e.response,
request_id=e.request_id, url=e.url, method=e.method,
http_status=e.http_status, cause=e.cause)
def _list(self, resource_type, value=None, paginated=False,
path_args=None, **query):

View File

@ -35,7 +35,6 @@ import copy
import itertools
import time
from keystoneauth1 import exceptions as ksa_exc
import six
from six.moves.urllib import parse as url_parse
@ -947,7 +946,7 @@ class Resource(collections.MutableMapping):
try:
if cls.allow_retrieve:
return cls.get_by_id(session, name_or_id, path_args=path_args)
except ksa_exc.NotFound:
except exceptions.NotFoundException:
pass
data = cls.list(session, path_args=path_args)
@ -1023,7 +1022,7 @@ def wait_for_delete(session, resource, interval, wait):
while total_sleep < wait:
try:
resource.get(session)
except ksa_exc.NotFound:
except exceptions.NotFoundException:
return resource
time.sleep(interval)
total_sleep += interval

View File

@ -12,15 +12,19 @@
"""
The :class:`~openstack.session.Session` overrides
:class:`~keystoneauth1.session.Session` to provide end point filtering.
:class:`~keystoneauth1.session.Session` to provide end point filtering and
mapping KSA exceptions to SDK exceptions.
"""
import re
from six.moves.urllib import parse
from keystoneauth1 import exceptions as _exceptions
from keystoneauth1 import session as _session
import openstack
from openstack import exceptions
from six.moves.urllib import parse
DEFAULT_USER_AGENT = "openstacksdk/%s" % openstack.__version__
VERSION_PATTERN = re.compile('/v\d[\d.]*')
@ -40,6 +44,29 @@ def parse_url(filt, url):
return url
def map_exceptions(func):
def map_exceptions_wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except _exceptions.HttpError as e:
if e.http_status == 404:
raise exceptions.NotFoundException(
message=e.message, details=e.details,
response=e.response, request_id=e.request_id,
url=e.url, method=e.method,
http_status=e.http_status, cause=e)
else:
raise exceptions.HttpException(
message=e.message, details=e.details,
response=e.response, request_id=e.request_id,
url=e.url, method=e.method,
http_status=e.http_status, cause=e)
except _exceptions.ClientException as e:
raise exceptions.SDKException(message=e.message, cause=e)
return map_exceptions_wrapper
class Session(_session.Session):
def __init__(self, profile, user_agent=None, **kwargs):
@ -66,7 +93,7 @@ class Session(_session.Session):
self.profile = profile
def get_endpoint(self, auth=None, interface=None, **kwargs):
"""Override get endpoint to automate endpoint filering"""
"""Override get endpoint to automate endpoint filtering"""
service_type = kwargs.get('service_type')
filt = self.profile.get_filter(service_type)
@ -74,3 +101,7 @@ class Session(_session.Session):
filt.interface = interface
url = super(Session, self).get_endpoint(auth, **filt.get_filter())
return parse_url(filt, url)
@map_exceptions
def request(self, *args, **kwargs):
return super(Session, self).request(*args, **kwargs)

View File

@ -13,13 +13,10 @@
import os
import tempfile
from keystoneauth1 import exceptions as ksa_exc
import mock
import os_client_config
import six
from openstack import connection
from openstack import exceptions
from openstack import profile
from openstack.tests.unit import base
@ -193,37 +190,3 @@ class TestConnection(base.TestCase):
authenticator=mock.Mock())
res = sot.authorize()
self.assertIsNone(res)
def test_authorize_not_authorized(self):
fake_session = mock.Mock()
ex_auth = ksa_exc.AuthorizationFailure("not authorized")
fake_session.get_auth_headers.side_effect = ex_auth
sot = connection.Connection(session=fake_session,
authenticator=mock.Mock())
ex = self.assertRaises(exceptions.HttpException, sot.authorize)
self.assertEqual(401, ex.status_code)
self.assertEqual('HttpException: Authentication Failure, not '
'authorized', six.text_type(ex))
def test_authorize_no_authplugin(self):
fake_session = mock.Mock()
ex_auth = ksa_exc.MissingAuthPlugin("missing auth plugin")
fake_session.get_auth_headers.side_effect = ex_auth
sot = connection.Connection(session=fake_session,
authenticator=mock.Mock())
ex = self.assertRaises(exceptions.HttpException, sot.authorize)
self.assertEqual(400, ex.status_code)
self.assertEqual('HttpException: Bad Request, missing auth plugin',
six.text_type(ex))
def test_authorize_other_exceptions(self):
fake_session = mock.Mock()
fake_session.get_auth_headers.side_effect = Exception()
sot = connection.Connection(session=fake_session,
authenticator=mock.Mock())
ex = self.assertRaises(exceptions.HttpException, sot.authorize)
self.assertEqual(500, ex.status_code)
self.assertEqual('HttpException: Unknown exception',
six.text_type(ex))

View File

@ -47,11 +47,11 @@ class Test_HttpException(testtools.TestCase):
self.assertEqual(self.message, exc.message)
self.assertEqual(details, exc.details)
def test_status_code(self):
status_code = 123
def test_http_status(self):
http_status = 123
exc = self.assertRaises(exceptions.HttpException,
self._do_raise, self.message,
status_code=status_code)
http_status=http_status)
self.assertEqual(self.message, exc.message)
self.assertEqual(status_code, exc.status_code)
self.assertEqual(http_status, exc.http_status)

View File

@ -13,8 +13,6 @@
import mock
import testtools
from keystoneauth1 import exceptions as ksa_exc
from openstack import exceptions
from openstack import proxy
from openstack import resource
@ -119,15 +117,15 @@ class TestProxyDelete(testtools.TestCase):
self.assertEqual(rv, self.fake_id)
def test_delete_ignore_missing(self):
self.res.delete.side_effect = ksa_exc.NotFound(message="test",
http_status=404)
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
rv = self.sot._delete(DeleteableResource, self.fake_id)
self.assertIsNone(rv)
def test_delete_ResourceNotFound(self):
self.res.delete.side_effect = ksa_exc.NotFound(message="test",
http_status=404)
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
self.assertRaisesRegexp(
exceptions.ResourceNotFound,
@ -137,7 +135,7 @@ class TestProxyDelete(testtools.TestCase):
def test_delete_HttpException(self):
self.res.delete.side_effect = exceptions.HttpException(
message="test", status_code=500)
message="test", http_status=500)
self.assertRaises(exceptions.HttpException, self.sot._delete,
DeleteableResource, self.res, ignore_missing=False)
@ -238,8 +236,8 @@ class TestProxyGet(testtools.TestCase):
self.assertEqual(rv, self.fake_result)
def test_get_not_found(self):
self.res.get.side_effect = ksa_exc.NotFound(message="test",
http_status=404)
self.res.get.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
self.assertRaisesRegexp(
exceptions.ResourceNotFound,

View File

@ -14,7 +14,6 @@ import copy
import json
import os
from keystoneauth1 import exceptions as ksa_exceptions
from keystoneauth1 import session
import mock
import requests
@ -1287,7 +1286,7 @@ class TestFind(base.TestCase):
def test_name(self):
self.mock_get.side_effect = [
ksa_exceptions.http.NotFound(),
exceptions.NotFoundException(),
FakeResponse({FakeResource.resources_key: [self.matrix]})
]
@ -1330,7 +1329,7 @@ class TestFind(base.TestCase):
dupe['id'] = 'different'
self.mock_get.side_effect = [
# Raise a 404 first so we get out of the ID search and into name.
ksa_exceptions.http.NotFound(),
exceptions.NotFoundException(),
FakeResponse({FakeResource.resources_key: [self.matrix, dupe]})
]
@ -1358,7 +1357,7 @@ class TestFind(base.TestCase):
def test_nada(self):
self.mock_get.side_effect = [
ksa_exceptions.http.NotFound(),
exceptions.NotFoundException(),
FakeResponse({FakeResource.resources_key: []})
]
@ -1366,7 +1365,7 @@ class TestFind(base.TestCase):
def test_no_name(self):
self.mock_get.side_effect = [
ksa_exceptions.http.NotFound(),
exceptions.NotFoundException(),
FakeResponse({FakeResource.resources_key: [self.matrix]})
]
FakeResource.name_attribute = None
@ -1375,7 +1374,7 @@ class TestFind(base.TestCase):
def test_nada_not_ignored(self):
self.mock_get.side_effect = [
ksa_exceptions.http.NotFound(),
exceptions.NotFoundException(),
FakeResponse({FakeResource.resources_key: []})
]
@ -1451,7 +1450,7 @@ class TestWaitForDelete(base.TestCase):
sot.get = mock.Mock()
sot.get.side_effect = [
sot,
ksa_exceptions.http.NotFound()]
exceptions.NotFoundException()]
self.assertEqual(sot, resource.wait_for_delete(sess, sot, 1, 2))

View File

@ -10,8 +10,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
import testtools
from keystoneauth1 import exceptions as _exceptions
from openstack import exceptions
from openstack.image import image_service
from openstack import session
@ -41,3 +45,43 @@ class TestSession(testtools.TestCase):
def test_user_agent_set(self):
sot = session.Session(None, user_agent="testing/123")
self.assertTrue(sot.user_agent.startswith("testing/123 openstacksdk"))
def test_map_exceptions_not_found_exception(self):
ksa_exc = _exceptions.HttpError(message="test", http_status=404)
func = mock.Mock(side_effect=ksa_exc)
os_exc = self.assertRaises(
exceptions.NotFoundException, session.map_exceptions(func))
self.assertIsInstance(os_exc, exceptions.NotFoundException)
self.assertEqual(ksa_exc.message, os_exc.message)
self.assertEqual(ksa_exc.http_status, os_exc.http_status)
self.assertEqual(ksa_exc, os_exc.cause)
def test_map_exceptions_http_exception(self):
ksa_exc = _exceptions.HttpError(message="test", http_status=400)
func = mock.Mock(side_effect=ksa_exc)
os_exc = self.assertRaises(
exceptions.HttpException, session.map_exceptions(func))
self.assertIsInstance(os_exc, exceptions.HttpException)
self.assertEqual(ksa_exc.message, os_exc.message)
self.assertEqual(ksa_exc.http_status, os_exc.http_status)
self.assertEqual(ksa_exc, os_exc.cause)
def test_map_exceptions_sdk_exception_1(self):
ksa_exc = _exceptions.ClientException()
func = mock.Mock(side_effect=ksa_exc)
os_exc = self.assertRaises(
exceptions.SDKException, session.map_exceptions(func))
self.assertIsInstance(os_exc, exceptions.SDKException)
self.assertEqual(ksa_exc, os_exc.cause)
def test_map_exceptions_sdk_exception_2(self):
ksa_exc = _exceptions.VersionNotAvailable()
func = mock.Mock(side_effect=ksa_exc)
os_exc = self.assertRaises(
exceptions.SDKException, session.map_exceptions(func))
self.assertIsInstance(os_exc, exceptions.SDKException)
self.assertEqual(ksa_exc, os_exc.cause)