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:
Lucas Alvares Gomes 2017-02-27 20:14:59 +00:00
parent fb5e1cc3f2
commit 56af7a5616
5 changed files with 125 additions and 12 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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()

View 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')

View File

@ -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')