Correctly decode error messages from ironic API
Knowing a status code is simply not enough for debugging. Change-Id: If1d3f182ab028948ff05aea7e8024d4e7bc3d53c
This commit is contained in:
parent
1a9491e651
commit
53dbc87a35
@ -117,6 +117,26 @@ class APIClient(object):
|
|||||||
def supports_auto_tls(self):
|
def supports_auto_tls(self):
|
||||||
return self._get_ironic_api_version() >= AGENT_VERIFY_CA_IRONIC_VERSION
|
return self._get_ironic_api_version() >= AGENT_VERIFY_CA_IRONIC_VERSION
|
||||||
|
|
||||||
|
def _error_from_response(self, response):
|
||||||
|
try:
|
||||||
|
body = response.json()
|
||||||
|
except ValueError:
|
||||||
|
text = response.text
|
||||||
|
else:
|
||||||
|
body = body.get('error_message', body)
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
# Old ironic format
|
||||||
|
try:
|
||||||
|
body = jsonutils.loads(body)
|
||||||
|
except ValueError:
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
text = (body.get('faultstring')
|
||||||
|
or body.get('title')
|
||||||
|
or response.text)
|
||||||
|
|
||||||
|
return 'Error %d: %s' % (response.status_code, text)
|
||||||
|
|
||||||
def heartbeat(self, uuid, advertise_address, advertise_protocol='http',
|
def heartbeat(self, uuid, advertise_address, advertise_protocol='http',
|
||||||
generated_cert=None):
|
generated_cert=None):
|
||||||
path = self.heartbeat_api.format(uuid=uuid)
|
path = self.heartbeat_api.format(uuid=uuid)
|
||||||
@ -148,11 +168,11 @@ class APIClient(object):
|
|||||||
raise errors.HeartbeatError(str(e))
|
raise errors.HeartbeatError(str(e))
|
||||||
|
|
||||||
if response.status_code == requests.codes.CONFLICT:
|
if response.status_code == requests.codes.CONFLICT:
|
||||||
data = jsonutils.loads(response.content)
|
error = self._error_from_response(response)
|
||||||
raise errors.HeartbeatConflictError(data.get('faultstring'))
|
raise errors.HeartbeatConflictError(error)
|
||||||
elif response.status_code != requests.codes.ACCEPTED:
|
elif response.status_code != requests.codes.ACCEPTED:
|
||||||
msg = 'Invalid status code: {}'.format(response.status_code)
|
error = self._error_from_response(response)
|
||||||
raise errors.HeartbeatError(msg)
|
raise errors.HeartbeatError(error)
|
||||||
|
|
||||||
def lookup_node(self, hardware_info, timeout, starting_interval,
|
def lookup_node(self, hardware_info, timeout, starting_interval,
|
||||||
node_uuid=None, max_interval=30):
|
node_uuid=None, max_interval=30):
|
||||||
@ -225,9 +245,10 @@ class APIClient(object):
|
|||||||
|
|
||||||
if response.status_code != requests.codes.OK:
|
if response.status_code != requests.codes.OK:
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
'Failed looking up node with addresses %r at %s, '
|
'Failed looking up node with addresses %r at %s. '
|
||||||
'status code: %s. Check if inspection has completed.',
|
'%s. Check if inspection has completed.',
|
||||||
params['addresses'], self.api_url, response.status_code,
|
params['addresses'], self.api_url,
|
||||||
|
self._error_from_response(response)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@ -32,7 +33,9 @@ CONF = cfg.CONF
|
|||||||
class FakeResponse(object):
|
class FakeResponse(object):
|
||||||
def __init__(self, content=None, status_code=200, headers=None):
|
def __init__(self, content=None, status_code=200, headers=None):
|
||||||
content = content or {}
|
content = content or {}
|
||||||
self.content = jsonutils.dumps(content)
|
self.text = json.dumps(content)
|
||||||
|
# TODO(dtantsur): remove in favour of using text/json()
|
||||||
|
self.content = self.text.encode('utf-8')
|
||||||
self._json = content
|
self._json = content
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
@ -260,10 +263,59 @@ class TestBaseIronicPythonAgent(base.IronicAgentTest):
|
|||||||
|
|
||||||
def test_heartbeat_invalid_status_code(self):
|
def test_heartbeat_invalid_status_code(self):
|
||||||
response = FakeResponse(status_code=404)
|
response = FakeResponse(status_code=404)
|
||||||
|
response.text = 'Not a JSON'
|
||||||
self.api_client.session.request = mock.Mock()
|
self.api_client.session.request = mock.Mock()
|
||||||
self.api_client.session.request.return_value = response
|
self.api_client.session.request.return_value = response
|
||||||
|
|
||||||
self.assertRaises(errors.HeartbeatError,
|
self.assertRaisesRegex(errors.HeartbeatError,
|
||||||
|
'Error 404: Not a JSON',
|
||||||
|
self.api_client.heartbeat,
|
||||||
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
|
|
||||||
|
def test_heartbeat_error_format_1(self):
|
||||||
|
response = FakeResponse(
|
||||||
|
status_code=404,
|
||||||
|
content={'error_message': '{"faultcode": "Client", '
|
||||||
|
'"faultstring": "Resource could not be found.", '
|
||||||
|
'"debuginfo": null}'})
|
||||||
|
self.api_client.session.request = mock.Mock()
|
||||||
|
self.api_client.session.request.return_value = response
|
||||||
|
|
||||||
|
self.assertRaisesRegex(errors.HeartbeatError,
|
||||||
|
'Error 404: Resource could not be found',
|
||||||
|
self.api_client.heartbeat,
|
||||||
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
|
|
||||||
|
def test_heartbeat_error_format_2(self):
|
||||||
|
response = FakeResponse(
|
||||||
|
status_code=404,
|
||||||
|
content={'error_message': {
|
||||||
|
"faultcode\\": "Client",
|
||||||
|
"faultstring": "Resource could not be found.",
|
||||||
|
"debuginfo": None}})
|
||||||
|
self.api_client.session.request = mock.Mock()
|
||||||
|
self.api_client.session.request.return_value = response
|
||||||
|
|
||||||
|
self.assertRaisesRegex(errors.HeartbeatError,
|
||||||
|
'Error 404: Resource could not be found',
|
||||||
|
self.api_client.heartbeat,
|
||||||
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
|
|
||||||
|
def test_heartbeat_error_format_3(self):
|
||||||
|
response = FakeResponse(
|
||||||
|
status_code=404,
|
||||||
|
content={'error_message': {
|
||||||
|
"code": 404,
|
||||||
|
"title": "Resource could not be found.",
|
||||||
|
"description": None}})
|
||||||
|
self.api_client.session.request = mock.Mock()
|
||||||
|
self.api_client.session.request.return_value = response
|
||||||
|
|
||||||
|
self.assertRaisesRegex(errors.HeartbeatError,
|
||||||
|
'Error 404: Resource could not be found',
|
||||||
self.api_client.heartbeat,
|
self.api_client.heartbeat,
|
||||||
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
advertise_address=('192.0.2.1', '9999'))
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
|
4
releasenotes/notes/ironic-error-97e76d9ddacff039.yaml
Normal file
4
releasenotes/notes/ironic-error-97e76d9ddacff039.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Correctly decodes error messages from ironic API.
|
Loading…
Reference in New Issue
Block a user