Let CLI print exception traceback from 'debuginfo'

Response 'debuginfo' is used to carry server traceback in case of
error. If 'debuginfo' is not empty, append it to exception message,
so traceback will be printed together with message.

Change-Id: Id7900b64ec7fc9f906253ef0e2d754c2baa76067
Closes-Bug: #1255590
This commit is contained in:
max_lobur 2013-11-28 16:38:13 +02:00
parent 8d40327e6b
commit 01c178de83
4 changed files with 106 additions and 13 deletions
ironicclient

@ -120,15 +120,17 @@ class HTTPClient(object):
base_url = _args[2]
return '%s/%s' % (base_url.rstrip('/'), url.lstrip('/'))
def _extract_error_message(self, body):
def _extract_error_json(self, body):
error_json = {}
try:
body_json = json.loads(body)
if 'error_message' in body_json:
body_json = json.loads(body_json['error_message'])
if 'faultstring' in body_json:
return body_json['faultstring']
raw_msg = body_json['error_message']
error_json = json.loads(raw_msg)
except ValueError:
pass
return {}
return error_json
def _http_request(self, url, method, **kwargs):
"""Send an http request with the specified characteristics.
@ -172,8 +174,10 @@ class HTTPClient(object):
if 400 <= resp.status < 600:
LOG.warn("Request returned failure status.")
err_msg = self._extract_error_message(body_str)
raise exc.from_response(resp, err_msg)
error_json = self._extract_error_json(body_str)
raise exc.from_response(resp,
error_json.get('faultstring'),
error_json.get('debuginfo'))
elif resp.status in (301, 302, 305):
# Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs)

@ -42,12 +42,18 @@ class HTTPException(ClientException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, details=None):
def __init__(self, details=None, server_traceback=None):
self.details = details
self.server_traceback = server_traceback
def __str__(self):
return self.details or "%s (HTTP %s)" % (self.__class__.__name__,
self.code)
msg = "%s (HTTP %s)" % (self.__class__.__name__,
self.code)
if self.details:
msg = self.details
if self.server_traceback:
msg += "\n%s" % self.server_traceback
return msg
class HTTPMultipleChoices(HTTPException):
@ -148,10 +154,10 @@ for obj_name in dir(sys.modules[__name__]):
_code_map[obj.code] = obj
def from_response(response, error=None):
def from_response(response, message=None, traceback=None):
"""Return an instance of an HTTPException based on httplib response."""
cls = _code_map.get(response.status, HTTPException)
return cls(error)
return cls(message, traceback)
class NoTokenLookupException(Exception):

@ -13,7 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import six
from ironicclient.common import http
from ironicclient import exc
from ironicclient.tests import utils
@ -38,3 +42,63 @@ class HttpClientTest(utils.BaseTestCase):
client = http.HTTPClient('http://localhost')
url = client._make_connection_url('v1/resources')
self.assertEqual(url, '/v1/resources')
@staticmethod
def _get_error_body(faultstring=None, debuginfo=None):
error_body = {
'faultstring': faultstring,
'debuginfo': debuginfo
}
raw_error_body = json.dumps(error_body)
body = {'error_message': raw_error_body}
raw_body = json.dumps(body)
return raw_body
def test_server_exception_empty_body(self):
error_body = self._get_error_body()
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = \
lambda *a, **kw: utils.FakeConnection(fake_resp)
error = self.assertRaises(exc.HTTPInternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual('HTTPInternalServerError (HTTP 500)', str(error))
def test_server_exception_msg_only(self):
error_msg = 'test error msg'
error_body = self._get_error_body(error_msg)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = \
lambda *a, **kw: utils.FakeConnection(fake_resp)
error = self.assertRaises(exc.HTTPInternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(error_msg, str(error))
def test_server_exception_msg_and_traceback(self):
error_msg = 'another test error'
error_trace = "\"Traceback (most recent call last):\\n\\n " \
"File \\\"/usr/local/lib/python2.7/..."
error_body = self._get_error_body(error_msg, error_trace)
fake_resp = utils.FakeResponse({'content-type': 'application/json'},
six.StringIO(error_body),
version=1,
status=500)
client = http.HTTPClient('http://localhost/')
client.get_connection = \
lambda *a, **kw: utils.FakeConnection(fake_resp)
error = self.assertRaises(exc.HTTPInternalServerError,
client.json_request,
'GET', '/v1/resources')
self.assertEqual(error_msg + "\n" + error_trace, str(error))

@ -49,13 +49,32 @@ class FakeAPI(object):
return FakeResponse(response[0]), response[1]
class FakeConnection(object):
def __init__(self, response=None):
self._response = response
self._last_request = None
def request(self, method, conn_url, **kwargs):
self._last_request = (method, conn_url, kwargs)
def setresponse(self, response):
self._response = response
def getresponse(self):
return self._response
class FakeResponse(object):
def __init__(self, headers, body=None, version=None):
def __init__(self, headers, body=None, version=None, status=None,
reason=None):
""":param headers: dict representing HTTP response headers
:param body: file-like object
"""
self.headers = headers
self.body = body
self.version = version
self.status = status
self.reason = reason
def getheaders(self):
return copy.deepcopy(self.headers).items()