Handle NotAcceptable when Accept-Encoding: identity is not allowed
Apparently, some HPE Gen 10 Plus machines do not allow identity encoding when BIOS registries are requested. Add a fallback to Connector. Change-Id: I7363df5f5f74705307990dda9dfc7baebd5c07a3
This commit is contained in:
parent
5dd160cb35
commit
be29045601
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Adds a work-around for cases where ``Accept-Encoding: identity`` is not
|
||||||
|
accepted.
|
|
@ -51,15 +51,6 @@ class Connector(object):
|
||||||
# By default, we ask HTTP server to shut down HTTP connection we've
|
# By default, we ask HTTP server to shut down HTTP connection we've
|
||||||
# just used.
|
# just used.
|
||||||
self._session.headers['Connection'] = 'close'
|
self._session.headers['Connection'] = 'close'
|
||||||
# NOTE(TheJulia): Depending on the BMC, offering compression as an
|
|
||||||
# acceptable response changes the ETag behavior to offering an
|
|
||||||
# automatic "weak" ETag response, which is appropriate because the
|
|
||||||
# body content *may* not be a byte for byte match given compression.
|
|
||||||
# Overall, the value of compression is less than the value of concise
|
|
||||||
# interaction with the BMC. Setting to identity basically means
|
|
||||||
# "without modification or compression". By default, python-requests
|
|
||||||
# indicates responses can be compressed.
|
|
||||||
self._session.headers['Accept-Encoding'] = 'identity'
|
|
||||||
|
|
||||||
if username or password:
|
if username or password:
|
||||||
LOG.warning('Passing username and password to Connector is '
|
LOG.warning('Passing username and password to Connector is '
|
||||||
|
@ -115,7 +106,8 @@ class Connector(object):
|
||||||
PUT, PATCH, etc...
|
PUT, PATCH, etc...
|
||||||
:param path: The sub-URI or absolute URL path to the resource.
|
:param path: The sub-URI or absolute URL path to the resource.
|
||||||
:param data: Optional JSON data.
|
:param data: Optional JSON data.
|
||||||
:param headers: Optional dictionary of headers.
|
:param headers: Optional dictionary of headers. Use None value
|
||||||
|
to remove a default header.
|
||||||
:param blocking: Whether to block for asynchronous operations.
|
:param blocking: Whether to block for asynchronous operations.
|
||||||
:param timeout: Max time in seconds to wait for blocking async call or
|
:param timeout: Max time in seconds to wait for blocking async call or
|
||||||
for requests library to connect and read. If a custom
|
for requests library to connect and read. If a custom
|
||||||
|
@ -138,12 +130,25 @@ class Connector(object):
|
||||||
|
|
||||||
url = path if urlparse.urlparse(path).netloc else urlparse.urljoin(
|
url = path if urlparse.urlparse(path).netloc else urlparse.urljoin(
|
||||||
self._url, path)
|
self._url, path)
|
||||||
headers = headers or {}
|
headers = (headers or {}).copy()
|
||||||
lc_headers = [k.lower() for k in headers]
|
lc_headers = [k.lower() for k in headers]
|
||||||
if data is not None and 'content-type' not in lc_headers:
|
if data is not None and 'content-type' not in lc_headers:
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
if 'odata-version' not in lc_headers:
|
if 'odata-version' not in lc_headers:
|
||||||
headers['OData-Version'] = '4.0'
|
headers['OData-Version'] = '4.0'
|
||||||
|
# NOTE(TheJulia): Depending on the BMC, offering compression as an
|
||||||
|
# acceptable response changes the ETag behavior to offering an
|
||||||
|
# automatic "weak" ETag response, which is appropriate because the
|
||||||
|
# body content *may* not be a byte for byte match given compression.
|
||||||
|
# Overall, the value of compression is less than the value of concise
|
||||||
|
# interaction with the BMC. Setting to identity basically means
|
||||||
|
# "without modification or compression". By default, python-requests
|
||||||
|
# indicates responses can be compressed.
|
||||||
|
if 'accept-encoding' not in lc_headers:
|
||||||
|
headers['Accept-Encoding'] = 'identity'
|
||||||
|
# Allow removing default headers
|
||||||
|
headers = {k: v for k, v in headers.items() if v is not None}
|
||||||
|
|
||||||
# TODO(lucasagomes): We should mask the data to remove sensitive
|
# TODO(lucasagomes): We should mask the data to remove sensitive
|
||||||
# information
|
# information
|
||||||
LOG.debug('HTTP request: %(method)s %(url)s; headers: %(headers)s; '
|
LOG.debug('HTTP request: %(method)s %(url)s; headers: %(headers)s; '
|
||||||
|
@ -268,6 +273,21 @@ class Connector(object):
|
||||||
**extra_session_req_kwargs)
|
**extra_session_req_kwargs)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
except exceptions.NotAcceptableError as e:
|
||||||
|
# NOTE(dtantsur): some HPE Gen 10 Plus machines do not allow
|
||||||
|
# identity encoding when fetching registries.
|
||||||
|
if (method.lower() == 'get'
|
||||||
|
and headers.get('Accept-Encoding') == 'identity'):
|
||||||
|
LOG.warning('Server has indicated a NotAcceptable for %s, '
|
||||||
|
'retrying without identity encoding', e)
|
||||||
|
headers = dict(headers, **{'Accept-Encoding': None})
|
||||||
|
return self._op(
|
||||||
|
method, path, data=data, headers=headers,
|
||||||
|
blocking=blocking, timeout=timeout,
|
||||||
|
server_side_retries_left=server_side_retries_left,
|
||||||
|
**extra_session_req_kwargs)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
if blocking and response.status_code == 202:
|
if blocking and response.status_code == 202:
|
||||||
if not response.headers.get('Location'):
|
if not response.headers.get('Location'):
|
||||||
m = ('HTTP response for %(method)s request to %(url)s '
|
m = ('HTTP response for %(method)s request to %(url)s '
|
||||||
|
|
|
@ -160,6 +160,10 @@ class AccessError(HTTPError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptableError(HTTPError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MissingXAuthToken(HTTPError):
|
class MissingXAuthToken(HTTPError):
|
||||||
message = ('No X-Auth-Token returned from remote host when '
|
message = ('No X-Auth-Token returned from remote host when '
|
||||||
'attempting to establish a session. Error: %(error)s')
|
'attempting to establish a session. Error: %(error)s')
|
||||||
|
@ -176,6 +180,8 @@ def raise_for_response(method, url, response):
|
||||||
elif response.status_code in (http_client.UNAUTHORIZED,
|
elif response.status_code in (http_client.UNAUTHORIZED,
|
||||||
http_client.FORBIDDEN):
|
http_client.FORBIDDEN):
|
||||||
raise AccessError(method, url, response)
|
raise AccessError(method, url, response)
|
||||||
|
elif response.status_code == http_client.NOT_ACCEPTABLE:
|
||||||
|
raise NotAcceptableError(method, url, response)
|
||||||
elif response.status_code >= http_client.INTERNAL_SERVER_ERROR:
|
elif response.status_code >= http_client.INTERNAL_SERVER_ERROR:
|
||||||
raise ServerSideError(method, url, response)
|
raise ServerSideError(method, url, response)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -168,18 +168,25 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
server_side_retries=10, server_side_retries_delay=3)
|
server_side_retries=10, server_side_retries_delay=3)
|
||||||
self.conn._auth = mock_auth
|
self.conn._auth = mock_auth
|
||||||
self.data = {'fake': 'data'}
|
self.data = {'fake': 'data'}
|
||||||
self.headers = {'X-Fake': 'header'}
|
self.headers = {'Accept-Encoding': 'identity', 'OData-Version': '4.0'}
|
||||||
self.session = mock.Mock(spec=requests.Session)
|
self.session = mock.Mock(spec=requests.Session)
|
||||||
self.conn._session = self.session
|
self.conn._session = self.session
|
||||||
self.request = self.session.request
|
self.request = self.session.request
|
||||||
self.request.return_value.status_code = http_client.OK
|
self.request.return_value.status_code = http_client.OK
|
||||||
|
|
||||||
def test_ok_get(self):
|
def test_ok_get(self):
|
||||||
self.conn._op('GET', path='fake/path', headers=self.headers)
|
self.conn._op('GET', path='fake/path')
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'GET', 'http://foo.bar:1234/fake/path',
|
'GET', 'http://foo.bar:1234/fake/path',
|
||||||
headers=self.headers, json=None, verify=True, timeout=60)
|
headers=self.headers, json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
|
def test_ok_get_with_headers(self):
|
||||||
|
self.conn._op('GET', path='fake/path', headers={'answer': '42'})
|
||||||
|
self.request.assert_called_once_with(
|
||||||
|
'GET', 'http://foo.bar:1234/fake/path',
|
||||||
|
headers=dict(self.headers, answer='42'),
|
||||||
|
json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
def test_response_callback(self):
|
def test_response_callback(self):
|
||||||
mock_response_callback = mock.MagicMock()
|
mock_response_callback = mock.MagicMock()
|
||||||
self.conn._response_callback = mock_response_callback
|
self.conn._response_callback = mock_response_callback
|
||||||
|
@ -188,34 +195,42 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.assertEqual(1, mock_response_callback.call_count)
|
self.assertEqual(1, mock_response_callback.call_count)
|
||||||
|
|
||||||
def test_ok_get_url_redirect_false(self):
|
def test_ok_get_url_redirect_false(self):
|
||||||
self.conn._op('GET', path='fake/path', headers=self.headers,
|
self.conn._op('GET', path='fake/path', allow_redirects=False)
|
||||||
allow_redirects=False)
|
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'GET', 'http://foo.bar:1234/fake/path',
|
'GET', 'http://foo.bar:1234/fake/path',
|
||||||
headers=self.headers, json=None, allow_redirects=False,
|
headers=self.headers, json=None, allow_redirects=False,
|
||||||
verify=True, timeout=60)
|
verify=True, timeout=60)
|
||||||
|
|
||||||
def test_ok_post(self):
|
def test_ok_post(self):
|
||||||
self.conn._op('POST', path='fake/path', data=self.data.copy(),
|
self.conn._op('POST', path='fake/path', data=self.data.copy())
|
||||||
headers=self.headers)
|
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'POST', 'http://foo.bar:1234/fake/path',
|
'POST', 'http://foo.bar:1234/fake/path',
|
||||||
json=self.data, headers=self.headers, verify=True, timeout=60)
|
json=self.data,
|
||||||
|
headers=dict(self.headers, **{'Content-Type': 'application/json'}),
|
||||||
|
verify=True, timeout=60)
|
||||||
|
|
||||||
|
def test_ok_post_with_headers(self):
|
||||||
|
self.conn._op('POST', path='fake/path', data=self.data.copy(),
|
||||||
|
headers={'answer': 42})
|
||||||
|
self.request.assert_called_once_with(
|
||||||
|
'POST', 'http://foo.bar:1234/fake/path',
|
||||||
|
json=self.data,
|
||||||
|
headers=dict(self.headers, **{'Content-Type': 'application/json',
|
||||||
|
'answer': 42}),
|
||||||
|
verify=True, timeout=60)
|
||||||
|
|
||||||
def test_ok_put(self):
|
def test_ok_put(self):
|
||||||
self.conn._op('PUT', path='fake/path', data=self.data.copy(),
|
self.conn._op('PUT', path='fake/path', data=self.data.copy())
|
||||||
headers=self.headers)
|
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'PUT', 'http://foo.bar:1234/fake/path',
|
'PUT', 'http://foo.bar:1234/fake/path',
|
||||||
json=self.data, headers=self.headers, verify=True, timeout=60)
|
headers=dict(self.headers, **{'Content-Type': 'application/json'}),
|
||||||
|
json=self.data, verify=True, timeout=60)
|
||||||
|
|
||||||
def test_ok_delete(self):
|
def test_ok_delete(self):
|
||||||
expected_headers = self.headers.copy()
|
self.conn._op('DELETE', path='fake/path')
|
||||||
expected_headers['OData-Version'] = '4.0'
|
|
||||||
self.conn._op('DELETE', path='fake/path', headers=self.headers.copy())
|
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'DELETE', 'http://foo.bar:1234/fake/path',
|
'DELETE', 'http://foo.bar:1234/fake/path',
|
||||||
headers=expected_headers, json=None, verify=True, timeout=60)
|
headers=self.headers, json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
def test_ok_post_with_session(self):
|
def test_ok_post_with_session(self):
|
||||||
self.conn._session.headers = {}
|
self.conn._session.headers = {}
|
||||||
|
@ -242,23 +257,24 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'GET', 'http://foo.bar:1234' + path,
|
'GET', 'http://foo.bar:1234' + path,
|
||||||
headers=expected_headers, json=None, verify=True, timeout=60)
|
headers=expected_headers, json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
def test_odata_version_header_redfish_no_headers(self):
|
|
||||||
path = '/redfish/v1/bar'
|
|
||||||
expected_headers = {'OData-Version': '4.0'}
|
|
||||||
self.conn._op('GET', path=path)
|
|
||||||
self.request.assert_called_once_with(
|
|
||||||
'GET', 'http://foo.bar:1234' + path,
|
|
||||||
headers=expected_headers, json=None, verify=True, timeout=60)
|
|
||||||
|
|
||||||
def test_odata_version_header_redfish_existing_header(self):
|
def test_odata_version_header_redfish_existing_header(self):
|
||||||
path = '/redfish/v1/foo'
|
path = '/redfish/v1/foo'
|
||||||
headers = {'OData-Version': '3.0'}
|
headers = {'OData-Version': '3.0'}
|
||||||
expected_headers = dict(headers)
|
expected_headers = dict(self.headers, **headers)
|
||||||
self.conn._op('GET', path=path, headers=headers)
|
self.conn._op('GET', path=path, headers=headers)
|
||||||
self.request.assert_called_once_with(
|
self.request.assert_called_once_with(
|
||||||
'GET', 'http://foo.bar:1234' + path,
|
'GET', 'http://foo.bar:1234' + path,
|
||||||
headers=expected_headers, json=None, verify=True, timeout=60)
|
headers=expected_headers, json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
|
def test_remove_header_accept_encoding(self):
|
||||||
|
path = '/redfish/v1/foo'
|
||||||
|
headers = {'Accept-Encoding': None}
|
||||||
|
self.headers.pop('Accept-Encoding')
|
||||||
|
self.conn._op('GET', path=path, headers=headers)
|
||||||
|
self.request.assert_called_once_with(
|
||||||
|
'GET', 'http://foo.bar:1234' + path,
|
||||||
|
headers=self.headers, json=None, verify=True, timeout=60)
|
||||||
|
|
||||||
def test_timed_out_session_unable_to_create_session(self):
|
def test_timed_out_session_unable_to_create_session(self):
|
||||||
self.conn._auth.can_refresh_session.return_value = False
|
self.conn._auth.can_refresh_session.return_value = False
|
||||||
self.session.auth = None
|
self.session.auth = None
|
||||||
|
@ -522,6 +538,29 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
self.assertEqual(0, mock_sleep.call_count)
|
self.assertEqual(0, mock_sleep.call_count)
|
||||||
self.assertEqual(1, self.request.call_count)
|
self.assertEqual(1, self.request.call_count)
|
||||||
|
|
||||||
|
def test_op_retry_without_identity(self):
|
||||||
|
self.request.side_effect = [
|
||||||
|
mock.Mock(status_code=http_client.NOT_ACCEPTABLE),
|
||||||
|
mock.Mock(status_code=http_client.OK),
|
||||||
|
]
|
||||||
|
self.conn._op('GET', 'http://foo.bar')
|
||||||
|
|
||||||
|
self.assertEqual(2, self.request.call_count)
|
||||||
|
headers_no_accept = self.headers.copy()
|
||||||
|
headers_no_accept.pop('Accept-Encoding')
|
||||||
|
self.request.assert_has_calls([
|
||||||
|
mock.call('GET', 'http://foo.bar', headers=self.headers,
|
||||||
|
json=None, verify=True, timeout=60),
|
||||||
|
mock.call('GET', 'http://foo.bar', headers=headers_no_accept,
|
||||||
|
json=None, verify=True, timeout=60),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_op_retry_without_identity_fails(self):
|
||||||
|
self.request.return_value.status_code = http_client.NOT_ACCEPTABLE
|
||||||
|
self.assertRaises(exceptions.NotAcceptableError, self.conn._op,
|
||||||
|
'GET', 'http://foo.bar')
|
||||||
|
self.assertEqual(2, self.request.call_count)
|
||||||
|
|
||||||
def test_access_error(self):
|
def test_access_error(self):
|
||||||
self.conn._auth = None
|
self.conn._auth = None
|
||||||
|
|
||||||
|
@ -715,8 +754,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header',
|
headers=dict(self.headers, **{'If-Match': '"3d7b8a7360bf2941d"'}),
|
||||||
'If-Match': '"3d7b8a7360bf2941d"'},
|
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
@ -738,8 +776,8 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header',
|
headers=dict(self.headers,
|
||||||
'If-Match': 'W/"3d7b8a7360bf2941d"'},
|
**{'If-Match': 'W/"3d7b8a7360bf2941d"'}),
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
@ -759,7 +797,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header'},
|
headers=self.headers,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
@ -787,8 +825,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header',
|
headers=dict(self.headers, **{'If-Match': '"3d7b8a7360bf2941d"'}),
|
||||||
'If-Match': '"3d7b8a7360bf2941d"'},
|
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
@ -818,7 +855,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header'},
|
headers=self.headers,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
@ -846,7 +883,7 @@ class ConnectorOpTestCase(base.TestCase):
|
||||||
'/redfish/v1/Systems/1',
|
'/redfish/v1/Systems/1',
|
||||||
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
data={'Boot': {'BootSourceOverrideTarget': 'Cd',
|
||||||
'BootSourceOverrideEnabled': 'Once'}},
|
'BootSourceOverrideEnabled': 'Once'}},
|
||||||
headers={'X-Fake': 'header'},
|
headers=self.headers,
|
||||||
blocking=False,
|
blocking=False,
|
||||||
timeout=60)
|
timeout=60)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue