Proper errors handling for client

* Introduce a specific exception class (subclasses requests.HTTPError
  to be backward compatible)
* Fetch error message from response body
* i18nize existing errors

Change-Id: I71922895c3177720789c6933da79b8882bfa7127
Closes-Bug: #1428680
This commit is contained in:
Dmitry Tantsur 2015-03-27 15:26:09 +01:00
parent aba4268f40
commit 0d839308cf
3 changed files with 53 additions and 11 deletions

View File

@ -364,6 +364,10 @@ See `1.1.0 release tracking page`_ for details.
* Proper CLI tool implemented as a plugin for OpenStackClient_. * Proper CLI tool implemented as a plugin for OpenStackClient_.
Also client now properly sets error message from the server in its exception.
This might be a breaking change, if you relied on exception message
previously.
* The default value for ``overwrite_existing`` configuration option was * The default value for ``overwrite_existing`` configuration option was
flipped, matching the default behavior for Ironic inspection. flipped, matching the default behavior for Ironic inspection.

View File

@ -19,8 +19,11 @@ import json
import requests import requests
import six import six
from ironic_discoverd.common.i18n import _
_DEFAULT_URL = 'http://127.0.0.1:5050/v1' _DEFAULT_URL = 'http://127.0.0.1:5050/v1'
_ERROR_ENCODING = 'utf-8'
def _prepare(base_url, auth_token): def _prepare(base_url, auth_token):
@ -31,6 +34,20 @@ def _prepare(base_url, auth_token):
return base_url, headers return base_url, headers
class ClientError(requests.HTTPError):
"""Error returned from a server."""
def __init__(self, response):
# discoverd returns error message in body
msg = response.content.decode(_ERROR_ENCODING)
super(ClientError, self).__init__(msg, response=response)
@classmethod
def raise_if_needed(cls, response):
"""Raise exception if response contains error."""
if response.status_code >= 400:
raise cls(response)
def introspect(uuid, base_url=None, auth_token=None, def introspect(uuid, base_url=None, auth_token=None,
new_ipmi_password=None, new_ipmi_username=None): new_ipmi_password=None, new_ipmi_username=None):
"""Start introspection for a node. """Start introspection for a node.
@ -46,16 +63,16 @@ def introspect(uuid, base_url=None, auth_token=None,
driver_info. driver_info.
""" """
if not isinstance(uuid, six.string_types): if not isinstance(uuid, six.string_types):
raise TypeError("Expected string for uuid argument, got %r" % uuid) raise TypeError(_("Expected string for uuid argument, got %r") % uuid)
if new_ipmi_username and not new_ipmi_password: if new_ipmi_username and not new_ipmi_password:
raise ValueError("Setting IPMI user name requires a new password") raise ValueError(_("Setting IPMI user name requires a new password"))
base_url, headers = _prepare(base_url, auth_token) base_url, headers = _prepare(base_url, auth_token)
params = {'new_ipmi_username': new_ipmi_username, params = {'new_ipmi_username': new_ipmi_username,
'new_ipmi_password': new_ipmi_password} 'new_ipmi_password': new_ipmi_password}
res = requests.post("%s/introspection/%s" % (base_url, uuid), res = requests.post("%s/introspection/%s" % (base_url, uuid),
headers=headers, params=params) headers=headers, params=params)
res.raise_for_status() ClientError.raise_if_needed(res)
def get_status(uuid, base_url=None, auth_token=None): def get_status(uuid, base_url=None, auth_token=None):
@ -69,12 +86,12 @@ def get_status(uuid, base_url=None, auth_token=None):
:raises: *requests* library HTTP errors. :raises: *requests* library HTTP errors.
""" """
if not isinstance(uuid, six.string_types): if not isinstance(uuid, six.string_types):
raise TypeError("Expected string for uuid argument, got %r" % uuid) raise TypeError(_("Expected string for uuid argument, got %r") % uuid)
base_url, headers = _prepare(base_url, auth_token) base_url, headers = _prepare(base_url, auth_token)
res = requests.get("%s/introspection/%s" % (base_url, uuid), res = requests.get("%s/introspection/%s" % (base_url, uuid),
headers=headers) headers=headers)
res.raise_for_status() ClientError.raise_if_needed(res)
return res.json() return res.json()
@ -84,14 +101,14 @@ def discover(uuids, base_url=None, auth_token=None):
DEPRECATED. Use introspect instead. DEPRECATED. Use introspect instead.
""" """
if not all(isinstance(s, six.string_types) for s in uuids): if not all(isinstance(s, six.string_types) for s in uuids):
raise TypeError("Expected list of strings for uuids argument, got %s" % raise TypeError(_("Expected list of strings for uuids argument, "
uuids) "got %s") % uuids)
base_url, headers = _prepare(base_url, auth_token) base_url, headers = _prepare(base_url, auth_token)
headers['Content-Type'] = 'application/json' headers['Content-Type'] = 'application/json'
res = requests.post(base_url + "/discover", res = requests.post(base_url + "/discover",
data=json.dumps(uuids), headers=headers) data=json.dumps(uuids), headers=headers)
res.raise_for_status() ClientError.raise_if_needed(res)
if __name__ == '__main__': # pragma: no cover if __name__ == '__main__': # pragma: no cover

View File

@ -19,7 +19,8 @@ from oslo_utils import uuidutils
from ironic_discoverd import client from ironic_discoverd import client
@mock.patch.object(client.requests, 'post', autospec=True) @mock.patch.object(client.requests, 'post', autospec=True,
**{'return_value.status_code': 200})
class TestIntrospect(unittest.TestCase): class TestIntrospect(unittest.TestCase):
def setUp(self): def setUp(self):
super(TestIntrospect, self).setUp() super(TestIntrospect, self).setUp()
@ -74,8 +75,15 @@ class TestIntrospect(unittest.TestCase):
params={'new_ipmi_username': None, 'new_ipmi_password': None} params={'new_ipmi_username': None, 'new_ipmi_password': None}
) )
def test_failed(self, mock_post):
mock_post.return_value.status_code = 404
mock_post.return_value.content = b"boom"
self.assertRaisesRegexp(client.ClientError, "boom",
client.introspect, self.uuid)
@mock.patch.object(client.requests, 'post', autospec=True)
@mock.patch.object(client.requests, 'post', autospec=True,
**{'return_value.status_code': 200})
class TestDiscover(unittest.TestCase): class TestDiscover(unittest.TestCase):
def setUp(self): def setUp(self):
super(TestDiscover, self).setUp() super(TestDiscover, self).setUp()
@ -97,8 +105,15 @@ class TestDiscover(unittest.TestCase):
self.assertRaises(TypeError, client.discover, 42) self.assertRaises(TypeError, client.discover, 42)
self.assertRaises(TypeError, client.discover, [42]) self.assertRaises(TypeError, client.discover, [42])
def test_failed(self, mock_post):
mock_post.return_value.status_code = 404
mock_post.return_value.content = b"boom"
self.assertRaisesRegexp(client.ClientError, "boom",
client.discover, [self.uuid])
@mock.patch.object(client.requests, 'get', autospec=True)
@mock.patch.object(client.requests, 'get', autospec=True,
**{'return_value.status_code': 200})
class TestGetStatus(unittest.TestCase): class TestGetStatus(unittest.TestCase):
def setUp(self): def setUp(self):
super(TestGetStatus, self).setUp() super(TestGetStatus, self).setUp()
@ -116,3 +131,9 @@ class TestGetStatus(unittest.TestCase):
def test_invalid_input(self, _): def test_invalid_input(self, _):
self.assertRaises(TypeError, client.get_status, 42) self.assertRaises(TypeError, client.get_status, 42)
def test_failed(self, mock_post):
mock_post.return_value.status_code = 404
mock_post.return_value.content = b"boom"
self.assertRaisesRegexp(client.ClientError, "boom",
client.get_status, self.uuid)