Rework exceptions
This patch is reworking the exceptions that can be raised from Sushy, before this patch many errors such as HTTP 4XX or 5XX were failing silently. This patch add custom exceptions for cases such as ConnectionError and HTTPError that can be raised from requests. Change-Id: I57ff8dffdfe0ab763bd0194e1916db8ae7a7861d
This commit is contained in:
parent
fb5e1cc3f2
commit
56af7a5616
@ -19,6 +19,8 @@ import os
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from sushy import exceptions
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +54,20 @@ class Connector(object):
|
|||||||
'the headers "%(headers)s" and data "%(data)s"',
|
'the headers "%(headers)s" and data "%(data)s"',
|
||||||
{'method': method, 'url': url, 'headers': headers,
|
{'method': method, 'url': url, 'headers': headers,
|
||||||
'data': data or ''})
|
'data': data or ''})
|
||||||
return session.request(method, url, data=data)
|
try:
|
||||||
|
response = session.request(method, url, data=data)
|
||||||
|
except requests.ConnectionError as e:
|
||||||
|
raise exceptions.ConnectionError(url=url, error=e)
|
||||||
|
|
||||||
|
LOG.debug('Response: Status code: %d', response.status_code)
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
raise exceptions.HTTPError(
|
||||||
|
method=method, url=url, error=e,
|
||||||
|
status_code=e.response.status_code)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def get(self, path='', data=None, headers=None):
|
def get(self, path='', data=None, headers=None):
|
||||||
return self._op('GET', path, data, headers)
|
return self._op('GET', path, data, headers)
|
||||||
|
@ -15,29 +15,49 @@
|
|||||||
|
|
||||||
|
|
||||||
class SushyError(Exception):
|
class SushyError(Exception):
|
||||||
|
"""Basic exception for errors raised by Sushy"""
|
||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
def __init__(self, message=None, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
if self.message and kwargs:
|
if self.message and kwargs:
|
||||||
self.message = self.message % kwargs
|
self.message = self.message % kwargs
|
||||||
else:
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
super(SushyError, self).__init__(self.message)
|
super(SushyError, self).__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
class ResourceNotFoundError(SushyError):
|
class ConnectionError(SushyError):
|
||||||
message = 'Resource %(resource)s not found'
|
message = 'Unable to connect to %(url)s. Error: %(error)s'
|
||||||
|
|
||||||
|
|
||||||
class MissingAttributeError(SushyError):
|
class MissingAttributeError(SushyError):
|
||||||
message = 'The attribute %(attribute)s is missing in %(resource)s'
|
message = ('The attribute %(attribute)s is missing from the '
|
||||||
|
'resource %(resource)s')
|
||||||
|
|
||||||
|
|
||||||
class MissingActionError(SushyError):
|
class MissingActionError(SushyError):
|
||||||
message = 'The action %(action)s is missing in %(resource)s'
|
message = ('The action %(action)s is missing from the '
|
||||||
|
'resource %(resource)s')
|
||||||
|
|
||||||
|
|
||||||
class InvalidParameterValueError(SushyError):
|
class InvalidParameterValueError(SushyError):
|
||||||
message = ('The parameter "%(parameter)s" value "%(value)s" is invalid. '
|
message = ('The parameter "%(parameter)s" value "%(value)s" is invalid. '
|
||||||
'Valid values are: %(valid_values)s')
|
'Valid values are: %(valid_values)s')
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPError(SushyError):
|
||||||
|
"""Basic exception for HTTP errors"""
|
||||||
|
|
||||||
|
status_code = None
|
||||||
|
message = ('Error issuing a %(method)s request at %(url)s. '
|
||||||
|
'Error: %(error)s')
|
||||||
|
|
||||||
|
def __init__(self, status_code=None, **kwargs):
|
||||||
|
super(HTTPError, self).__init__(**kwargs)
|
||||||
|
if status_code is not None:
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundError(HTTPError):
|
||||||
|
status_code = 404
|
||||||
|
message = 'Resource %(resource)s not found'
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
import abc
|
import abc
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from six.moves import http_client
|
|
||||||
|
|
||||||
from sushy import exceptions
|
from sushy import exceptions
|
||||||
|
|
||||||
@ -31,9 +30,12 @@ class ResourceBase(object):
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
resp = self._conn.get(path=self._path)
|
try:
|
||||||
if resp.status_code == http_client.NOT_FOUND:
|
resp = self._conn.get(path=self._path)
|
||||||
raise exceptions.ResourceNotFoundError(resource=self._path)
|
except exceptions.HTTPError as e:
|
||||||
|
if e.status_code == 404:
|
||||||
|
raise exceptions.ResourceNotFoundError(resource=self._path)
|
||||||
|
raise
|
||||||
|
|
||||||
self._json = resp.json()
|
self._json = resp.json()
|
||||||
self._parse_attributes()
|
self._parse_attributes()
|
||||||
|
55
sushy/tests/unit/resources/test_base.py
Normal file
55
sushy/tests/unit/resources/test_base.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Copyright 2017 Red Hat, Inc.
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from sushy import exceptions
|
||||||
|
from sushy.resources import base as resource_base
|
||||||
|
from sushy.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResouce(resource_base.ResourceBase):
|
||||||
|
|
||||||
|
def _parse_attributes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceBaseTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ResourceBaseTestCase, self).setUp()
|
||||||
|
self.conn = mock.Mock()
|
||||||
|
self.base_resource = BaseResouce(connector=self.conn, path='/Foo')
|
||||||
|
# refresh() is called in the constructor
|
||||||
|
self.conn.reset_mock()
|
||||||
|
|
||||||
|
def test_refresh(self):
|
||||||
|
self.base_resource.refresh()
|
||||||
|
self.conn.get.assert_called_once_with(path='/Foo')
|
||||||
|
|
||||||
|
def test_refresh_http_error_reraised(self):
|
||||||
|
self.conn.get.side_effect = exceptions.HTTPError(
|
||||||
|
method='GET', url='http://foo.bar:8000/redfish/v1', error='boom',
|
||||||
|
status_code=404)
|
||||||
|
self.assertRaises(exceptions.ResourceNotFoundError,
|
||||||
|
self.base_resource.refresh)
|
||||||
|
self.conn.get.assert_called_once_with(path='/Foo')
|
||||||
|
|
||||||
|
def test_refresh_resource_not_found(self):
|
||||||
|
self.conn.get.side_effect = exceptions.HTTPError(
|
||||||
|
method='GET', url='http://foo.bar:8000/redfish/v1', error='boom',
|
||||||
|
status_code=400)
|
||||||
|
self.assertRaises(exceptions.HTTPError, self.base_resource.refresh)
|
||||||
|
self.conn.get.assert_called_once_with(path='/Foo')
|
@ -14,8 +14,10 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
import requests
|
||||||
|
|
||||||
from sushy import connector
|
from sushy import connector
|
||||||
|
from sushy import exceptions
|
||||||
from sushy.tests.unit import base
|
from sushy.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
@ -68,3 +70,22 @@ class ConnectorTestCase(base.TestCase):
|
|||||||
def test__op_no_headers(self):
|
def test__op_no_headers(self):
|
||||||
expected_headers = {'Content-Type': 'application/json'}
|
expected_headers = {'Content-Type': 'application/json'}
|
||||||
self._test_op(None, expected_headers)
|
self._test_op(None, expected_headers)
|
||||||
|
|
||||||
|
@mock.patch('sushy.connector.requests.Session', autospec=True)
|
||||||
|
def test__op_connection_error(self, mock_session):
|
||||||
|
fake_session = mock.Mock()
|
||||||
|
mock_session.return_value.__enter__.return_value = fake_session
|
||||||
|
fake_session.request.side_effect = requests.exceptions.ConnectionError
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.ConnectionError, self.conn._op, 'GET')
|
||||||
|
|
||||||
|
@mock.patch('sushy.connector.requests.Session', autospec=True)
|
||||||
|
def test__op_http_error(self, mock_session):
|
||||||
|
fake_session = mock.Mock()
|
||||||
|
mock_session.return_value.__enter__.return_value = fake_session
|
||||||
|
fake_response = fake_session.request.return_value
|
||||||
|
fake_response.status_code = 400
|
||||||
|
fake_response.raise_for_status.side_effect = (
|
||||||
|
requests.exceptions.HTTPError(response=fake_response))
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.HTTPError, self.conn._op, 'GET')
|
||||||
|
Loading…
Reference in New Issue
Block a user