
Currently, during the ironic shell client construction, if only os_auth_token and endpoint arguments are passed, custom HTTPClient class based on requests' sessions is used. This is unnecessary, as there is admin_token auth type in keystoneauth that does basically the same, eliminating the need for our custom implementation. Apart from that, there is a none auth, which requires only passing the desired endpoint to use, so we can use it too without having to specify fake token strings anymore. Let's use these auth methods instead and deprecate HTTPClient. Also this patch deprecates a bunch of arguments to client.get_client function, changing them to the standard keystoneauth naming. DocImpact Story: 1696791 Task: 11836 Depends-On: https://review.openstack.org/559116 Change-Id: Ifc7b45d047c8882a41021e1604b74d17eac2e6e8
954 lines
40 KiB
Python
954 lines
40 KiB
Python
# Copyright 2012 OpenStack LLC.
|
|
# 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 time
|
|
|
|
import mock
|
|
from oslo_serialization import jsonutils
|
|
import requests
|
|
import six
|
|
from six.moves import http_client
|
|
|
|
from keystoneauth1 import exceptions as kexc
|
|
|
|
from ironicclient.common import filecache
|
|
from ironicclient.common import http
|
|
from ironicclient import exc
|
|
from ironicclient.tests.unit import utils
|
|
|
|
|
|
DEFAULT_TIMEOUT = 600
|
|
|
|
DEFAULT_HOST = 'localhost'
|
|
DEFAULT_PORT = '1234'
|
|
|
|
|
|
def _get_error_body(faultstring=None, debuginfo=None, description=None):
|
|
if description:
|
|
error_body = {'description': description}
|
|
else:
|
|
error_body = {
|
|
'faultstring': faultstring,
|
|
'debuginfo': debuginfo
|
|
}
|
|
raw_error_body = jsonutils.dump_as_bytes(error_body)
|
|
body = {'error_message': raw_error_body}
|
|
return jsonutils.dumps(body)
|
|
|
|
|
|
def _session_client(**kwargs):
|
|
return http.SessionClient(os_ironic_api_version='1.6',
|
|
api_version_select_state='default',
|
|
max_retries=5,
|
|
retry_interval=2,
|
|
auth=None,
|
|
interface='publicURL',
|
|
service_type='baremetal',
|
|
region_name='',
|
|
endpoint='http://%s:%s' % (DEFAULT_HOST,
|
|
DEFAULT_PORT),
|
|
**kwargs)
|
|
|
|
|
|
class VersionNegotiationMixinTest(utils.BaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(VersionNegotiationMixinTest, self).setUp()
|
|
self.test_object = http.VersionNegotiationMixin()
|
|
self.test_object.os_ironic_api_version = '1.6'
|
|
self.test_object.api_version_select_state = 'default'
|
|
self.test_object.endpoint = "http://localhost:1234"
|
|
self.mock_mcu = mock.MagicMock()
|
|
self.test_object._make_connection_url = self.mock_mcu
|
|
self.response = utils.FakeResponse(
|
|
{}, status=http_client.NOT_ACCEPTABLE)
|
|
self.test_object.get_server = mock.MagicMock(
|
|
return_value=('localhost', '1234'))
|
|
|
|
def test__generic_parse_version_headers_has_headers(self):
|
|
response = {'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': '1.6',
|
|
}
|
|
expected = ('1.1', '1.6')
|
|
result = self.test_object._generic_parse_version_headers(response.get)
|
|
self.assertEqual(expected, result)
|
|
|
|
def test__generic_parse_version_headers_missing_headers(self):
|
|
response = {}
|
|
expected = (None, None)
|
|
result = self.test_object._generic_parse_version_headers(response.get)
|
|
self.assertEqual(expected, result)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
def test_negotiate_version_bad_state(self, mock_save_data):
|
|
# Test if bad api_version_select_state value
|
|
self.test_object.api_version_select_state = 'word of the day: augur'
|
|
self.assertRaises(
|
|
RuntimeError,
|
|
self.test_object.negotiate_version,
|
|
None, None)
|
|
self.assertEqual(0, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_older(self, mock_pvh, mock_save_data):
|
|
# Test newer client and older server
|
|
latest_ver = '1.5'
|
|
mock_pvh.return_value = ('1.1', latest_ver)
|
|
mock_conn = mock.MagicMock()
|
|
result = self.test_object.negotiate_version(mock_conn, self.response)
|
|
self.assertEqual(latest_ver, result)
|
|
self.assertEqual(1, mock_pvh.call_count)
|
|
host, port = http.get_server(self.test_object.endpoint)
|
|
mock_save_data.assert_called_once_with(host=host, port=port,
|
|
data=latest_ver)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_newer(self, mock_pvh, mock_save_data):
|
|
# Test newer server and older client
|
|
mock_pvh.return_value = ('1.1', '1.10')
|
|
mock_conn = mock.MagicMock()
|
|
result = self.test_object.negotiate_version(mock_conn, self.response)
|
|
self.assertEqual('1.6', result)
|
|
self.assertEqual(1, mock_pvh.call_count)
|
|
mock_save_data.assert_called_once_with(host=DEFAULT_HOST,
|
|
port=DEFAULT_PORT,
|
|
data='1.6')
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_no_version_on_error(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# Test older Ironic version which errored with no version number and
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = iter([(None, None), ('1.1', '1.2')])
|
|
mock_conn = mock.MagicMock()
|
|
result = self.test_object.negotiate_version(mock_conn, self.response)
|
|
self.assertEqual('1.2', result)
|
|
self.assertTrue(mock_msr.called)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(1, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_explicit_too_high(self, mock_pvh,
|
|
mock_save_data):
|
|
# requested version is not supported because it is too large
|
|
mock_pvh.return_value = ('1.1', '1.6')
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
self.test_object.os_ironic_api_version = '99.99'
|
|
self.assertRaises(
|
|
exc.UnsupportedVersion,
|
|
self.test_object.negotiate_version,
|
|
mock_conn, self.response)
|
|
self.assertEqual(1, mock_pvh.call_count)
|
|
self.assertEqual(0, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_explicit_not_supported(self, mock_pvh,
|
|
mock_save_data):
|
|
# requested version is supported by the server but the server returned
|
|
# 406 because the requested operation is not supported with the
|
|
# requested version
|
|
mock_pvh.return_value = ('1.1', '1.6')
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'negotiated'
|
|
self.test_object.os_ironic_api_version = '1.5'
|
|
self.assertRaises(
|
|
exc.UnsupportedVersion,
|
|
self.test_object.negotiate_version,
|
|
mock_conn, self.response)
|
|
self.assertEqual(1, mock_pvh.call_count)
|
|
self.assertEqual(0, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_strict_version_comparison(self, mock_pvh,
|
|
mock_save_data):
|
|
# Test version comparison with StrictVersion
|
|
max_ver = '1.10'
|
|
mock_pvh.return_value = ('1.2', max_ver)
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.os_ironic_api_version = '1.10'
|
|
result = self.test_object.negotiate_version(mock_conn, self.response)
|
|
self.assertEqual(max_ver, result)
|
|
self.assertEqual(1, mock_pvh.call_count)
|
|
host, port = http.get_server(self.test_object.endpoint)
|
|
mock_save_data.assert_called_once_with(host=host, port=port,
|
|
data=max_ver)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_user_latest(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = iter([(None, None), ('1.1', '1.99')])
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
self.test_object.os_ironic_api_version = 'latest'
|
|
result = self.test_object.negotiate_version(mock_conn, None)
|
|
self.assertEqual(http.LATEST_VERSION, result)
|
|
self.assertEqual('negotiated',
|
|
self.test_object.api_version_select_state)
|
|
self.assertEqual(http.LATEST_VERSION,
|
|
self.test_object.os_ironic_api_version)
|
|
|
|
self.assertTrue(mock_msr.called)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(1, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_user_list(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = [(None, None), ('1.1', '1.26')]
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
self.test_object.os_ironic_api_version = ['1.1', '1.6', '1.25',
|
|
'1.26', '1.26.1', '1.27',
|
|
'1.30']
|
|
result = self.test_object.negotiate_version(mock_conn, self.response)
|
|
self.assertEqual('1.26', result)
|
|
self.assertEqual('negotiated',
|
|
self.test_object.api_version_select_state)
|
|
self.assertEqual('1.26',
|
|
self.test_object.os_ironic_api_version)
|
|
|
|
self.assertTrue(mock_msr.called)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(1, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_user_list_fails_nomatch(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = iter([(None, None), ('1.2', '1.26')])
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
self.test_object.os_ironic_api_version = ['1.39', '1.1']
|
|
self.assertRaises(
|
|
exc.UnsupportedVersion,
|
|
self.test_object.negotiate_version,
|
|
mock_conn, self.response)
|
|
self.assertEqual('user',
|
|
self.test_object.api_version_select_state)
|
|
self.assertEqual(['1.39', '1.1'],
|
|
self.test_object.os_ironic_api_version)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(0, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_user_list_single_value(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = iter([(None, None), ('1.1', '1.26')])
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
# NOTE(TheJulia): Lets test this value explicitly because the
|
|
# minor number is actually the same.
|
|
self.test_object.os_ironic_api_version = ['1.01']
|
|
result = self.test_object.negotiate_version(mock_conn, None)
|
|
self.assertEqual('1.1', result)
|
|
self.assertEqual('negotiated',
|
|
self.test_object.api_version_select_state)
|
|
self.assertEqual('1.1',
|
|
self.test_object.os_ironic_api_version)
|
|
self.assertTrue(mock_msr.called)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(1, mock_save_data.call_count)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_make_simple_request',
|
|
autospec=True)
|
|
@mock.patch.object(http.VersionNegotiationMixin, '_parse_version_headers',
|
|
autospec=True)
|
|
def test_negotiate_version_server_user_list_fails_latest(
|
|
self, mock_pvh, mock_msr, mock_save_data):
|
|
# have to retry with simple get
|
|
mock_pvh.side_effect = iter([(None, None), ('1.1', '1.2')])
|
|
mock_conn = mock.MagicMock()
|
|
self.test_object.api_version_select_state = 'user'
|
|
self.test_object.os_ironic_api_version = ['1.01', 'latest']
|
|
self.assertRaises(
|
|
ValueError,
|
|
self.test_object.negotiate_version,
|
|
mock_conn, self.response)
|
|
self.assertEqual('user',
|
|
self.test_object.api_version_select_state)
|
|
self.assertEqual(['1.01', 'latest'],
|
|
self.test_object.os_ironic_api_version)
|
|
self.assertEqual(2, mock_pvh.call_count)
|
|
self.assertEqual(0, mock_save_data.call_count)
|
|
|
|
def test_get_server(self):
|
|
host = 'ironic-host'
|
|
port = '6385'
|
|
endpoint = 'http://%s:%s/ironic/v1/' % (host, port)
|
|
self.assertEqual((host, port), http.get_server(endpoint))
|
|
|
|
|
|
class HttpClientTest(utils.BaseTestCase):
|
|
|
|
@mock.patch.object(http.LOG, 'warning', autospec=True)
|
|
def test_http_client_deprecation(self, log_mock):
|
|
http.HTTPClient('http://localhost')
|
|
self.assertIn('deprecated', log_mock.call_args[0][0])
|
|
|
|
def test_url_generation_trailing_slash_in_base(self):
|
|
client = http.HTTPClient('http://localhost/')
|
|
url = client._make_connection_url('/v1/resources')
|
|
self.assertEqual('http://localhost/v1/resources', url)
|
|
|
|
def test_url_generation_without_trailing_slash_in_base(self):
|
|
client = http.HTTPClient('http://localhost')
|
|
url = client._make_connection_url('/v1/resources')
|
|
self.assertEqual('http://localhost/v1/resources', url)
|
|
|
|
def test_url_generation_without_prefix_slash_in_path(self):
|
|
client = http.HTTPClient('http://localhost')
|
|
url = client._make_connection_url('v1/resources')
|
|
self.assertEqual('http://localhost/v1/resources', url)
|
|
|
|
def test_server_https_request_with_application_octet_stream(self):
|
|
client = http.HTTPClient('https://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/octet-stream'},
|
|
"Body",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
|
|
response, body = client.json_request('GET', '/v1/resources')
|
|
self.assertEqual(client.session.request.return_value, response)
|
|
self.assertIsNone(body)
|
|
|
|
def test_server_exception_empty_body(self):
|
|
error_body = _get_error_body()
|
|
client = http.HTTPClient('http://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.INTERNAL_SERVER_ERROR)
|
|
|
|
self.assertRaises(exc.InternalServerError,
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_server_exception_msg_only(self):
|
|
error_msg = 'test error msg'
|
|
error_body = _get_error_body(error_msg)
|
|
client = http.HTTPClient('http://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.INTERNAL_SERVER_ERROR)
|
|
|
|
self.assertRaises(exc.InternalServerError,
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_server_exception_description_only(self):
|
|
error_msg = 'test error msg'
|
|
error_body = _get_error_body(description=error_msg)
|
|
client = http.HTTPClient('http://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.BAD_REQUEST)
|
|
|
|
self.assertRaisesRegex(exc.BadRequest, 'test error msg',
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_server_https_request_ok(self):
|
|
client = http.HTTPClient('https://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
"Body",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
|
|
client.json_request('GET', '/v1/resources')
|
|
|
|
def test_server_https_empty_body(self):
|
|
error_body = _get_error_body()
|
|
|
|
client = http.HTTPClient('https://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.INTERNAL_SERVER_ERROR)
|
|
|
|
self.assertRaises(exc.InternalServerError,
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_401_unauthorized_exception(self):
|
|
error_body = _get_error_body()
|
|
client = http.HTTPClient('http://localhost/')
|
|
client.session = utils.mockSession(
|
|
{'Content-Type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.UNAUTHORIZED)
|
|
|
|
self.assertRaises(exc.Unauthorized, client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_http_request_not_valid_request(self):
|
|
client = http.HTTPClient('http://localhost/')
|
|
client.session.request = mock.Mock(
|
|
side_effect=http.requests.exceptions.InvalidSchema)
|
|
|
|
self.assertRaises(exc.ValidationError, client._http_request,
|
|
'http://localhost/', 'GET')
|
|
|
|
def test__parse_version_headers(self):
|
|
# Test parsing of version headers from HTTPClient
|
|
error_body = _get_error_body()
|
|
expected_result = ('1.1', '1.6')
|
|
|
|
client = http.HTTPClient('http://localhost/')
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': '1.6',
|
|
'Content-Type': 'text/plain',
|
|
},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.NOT_ACCEPTABLE)
|
|
result = client._parse_version_headers(fake_resp)
|
|
self.assertEqual(expected_result, result)
|
|
|
|
@mock.patch.object(filecache, 'save_data', autospec=True)
|
|
def test__http_request_client_fallback_fail(self, mock_save_data):
|
|
# Test when fallback to a supported version fails
|
|
host, port, latest_ver = 'localhost', '1234', '1.6'
|
|
error_body = _get_error_body()
|
|
|
|
client = http.HTTPClient('http://%s:%s/' % (host, port))
|
|
client.session = utils.mockSession(
|
|
{'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': latest_ver,
|
|
'content-type': 'text/plain',
|
|
},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.NOT_ACCEPTABLE)
|
|
self.assertRaises(
|
|
exc.UnsupportedVersion,
|
|
client._http_request,
|
|
'/v1/resources',
|
|
'GET')
|
|
mock_save_data.assert_called_once_with(host=host, data=latest_ver,
|
|
port=port)
|
|
|
|
@mock.patch.object(http.VersionNegotiationMixin, 'negotiate_version',
|
|
autospec=False)
|
|
def test__http_request_client_fallback_success(self, mock_negotiate):
|
|
# Test when fallback to a supported version succeeds
|
|
mock_negotiate.return_value = '1.6'
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': '1.6',
|
|
'content-type': 'text/plain',
|
|
},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.NOT_ACCEPTABLE)
|
|
good_resp = utils.mockSessionResponse(
|
|
{'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': '1.6',
|
|
'content-type': 'text/plain',
|
|
},
|
|
"We got some text",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
client = http.HTTPClient('http://localhost/')
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
|
|
mock_session.request.side_effect = iter([bad_resp, good_resp])
|
|
response, body_iter = client._http_request('/v1/resources', 'GET')
|
|
|
|
self.assertEqual(http_client.OK, response.status_code)
|
|
self.assertEqual(1, mock_negotiate.call_count)
|
|
|
|
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
|
def test_log_curl_request_mask_password(self, mock_log):
|
|
client = http.HTTPClient('http://localhost/')
|
|
kwargs = {'headers': {'foo-header': 'bar-header'},
|
|
'body': '{"password": "foo"}'}
|
|
client.log_curl_request('foo', '/v1/nodes', kwargs)
|
|
expected_log = ("curl -i -X foo -H 'foo-header: bar-header' "
|
|
"-d '{\"password\": \"***\"}' "
|
|
"http://localhost/v1/nodes")
|
|
mock_log.assert_called_once_with(expected_log)
|
|
|
|
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
|
def test_log_http_response_mask_password(self, mock_log):
|
|
client = http.HTTPClient('http://localhost/')
|
|
fake_response = utils.FakeResponse({}, version=1, reason='foo',
|
|
status=200)
|
|
body = '{"password": "foo"}'
|
|
client.log_http_response(fake_response, body=body)
|
|
expected_log = ("\nHTTP/0.1 200 foo\n\n{\"password\": \"***\"}\n")
|
|
mock_log.assert_called_once_with(expected_log)
|
|
|
|
def test__https_init_ssl_args_insecure(self):
|
|
client = http.HTTPClient('https://localhost/', insecure=True)
|
|
|
|
self.assertEqual(False, client.session.verify)
|
|
|
|
def test__https_init_ssl_args_secure(self):
|
|
client = http.HTTPClient('https://localhost/', ca_file='test_ca',
|
|
key_file='test_key', cert_file='test_cert')
|
|
|
|
self.assertEqual('test_ca', client.session.verify)
|
|
self.assertEqual(('test_cert', 'test_key'), client.session.cert)
|
|
|
|
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
|
def test_log_curl_request_with_body_and_header(self, mock_log):
|
|
client = http.HTTPClient('http://test')
|
|
headers = {'header1': 'value1'}
|
|
body = 'example body'
|
|
|
|
client.log_curl_request('GET', '/v1/nodes',
|
|
{'headers': headers, 'body': body})
|
|
|
|
self.assertTrue(mock_log.called)
|
|
self.assertTrue(mock_log.call_args[0])
|
|
self.assertEqual("curl -i -X GET -H 'header1: value1'"
|
|
" -d 'example body' http://test/v1/nodes",
|
|
mock_log.call_args[0][0])
|
|
|
|
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
|
def test_log_curl_request_with_certs(self, mock_log):
|
|
headers = {'header1': 'value1'}
|
|
client = http.HTTPClient('https://test', key_file='key',
|
|
cert_file='cert', cacert='cacert',
|
|
token='fake-token')
|
|
|
|
client.log_curl_request('GET', '/v1/test', {'headers': headers})
|
|
|
|
self.assertTrue(mock_log.called)
|
|
self.assertTrue(mock_log.call_args[0])
|
|
|
|
self.assertEqual("curl -i -X GET -H 'header1: value1' "
|
|
"--cert cert --key key https://test/v1/test",
|
|
mock_log.call_args[0][0])
|
|
|
|
@mock.patch.object(http.LOG, 'debug', autospec=True)
|
|
def test_log_curl_request_with_insecure_param(self, mock_log):
|
|
headers = {'header1': 'value1'}
|
|
http_client_object = http.HTTPClient('https://test', insecure=True,
|
|
token='fake-token')
|
|
|
|
http_client_object.log_curl_request('GET', '/v1/test',
|
|
{'headers': headers})
|
|
|
|
self.assertTrue(mock_log.called)
|
|
self.assertTrue(mock_log.call_args[0])
|
|
self.assertEqual("curl -i -X GET -H 'header1: value1' -k "
|
|
"--cert None --key None https://test/v1/test",
|
|
mock_log.call_args[0][0])
|
|
|
|
|
|
class SessionClientTest(utils.BaseTestCase):
|
|
|
|
@mock.patch.object(http.LOG, 'warning', autospec=True)
|
|
def test_session_client_endpoint_deprecation(self, log_mock):
|
|
http.SessionClient(os_ironic_api_version=1, session=mock.Mock(),
|
|
api_version_select_state='user', max_retries=5,
|
|
retry_interval=5, endpoint='abc')
|
|
self.assertIn('deprecated', log_mock.call_args[0][0])
|
|
|
|
def test_server_exception_empty_body(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_session = utils.mockSession({'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.INTERNAL_SERVER_ERROR)
|
|
|
|
client = _session_client(session=fake_session)
|
|
|
|
self.assertRaises(exc.InternalServerError,
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test_server_exception_description_only(self):
|
|
error_msg = 'test error msg'
|
|
error_body = _get_error_body(description=error_msg)
|
|
fake_session = utils.mockSession(
|
|
{'Content-Type': 'application/json'},
|
|
error_body, status_code=http_client.BAD_REQUEST)
|
|
client = _session_client(session=fake_session)
|
|
|
|
self.assertRaisesRegex(exc.BadRequest, 'test error msg',
|
|
client.json_request,
|
|
'GET', '/v1/resources')
|
|
|
|
def test__parse_version_headers(self):
|
|
# Test parsing of version headers from SessionClient
|
|
fake_session = utils.mockSession(
|
|
{'X-OpenStack-Ironic-API-Minimum-Version': '1.1',
|
|
'X-OpenStack-Ironic-API-Maximum-Version': '1.6',
|
|
'content-type': 'text/plain',
|
|
},
|
|
None,
|
|
http_client.HTTP_VERSION_NOT_SUPPORTED)
|
|
expected_result = ('1.1', '1.6')
|
|
client = _session_client(session=fake_session)
|
|
result = client._parse_version_headers(fake_session.request())
|
|
self.assertEqual(expected_result, result)
|
|
|
|
def _test_endpoint_override(self, endpoint):
|
|
fake_session = utils.mockSession({'content-type': 'application/json'},
|
|
status_code=http_client.NO_CONTENT)
|
|
request_mock = mock.Mock()
|
|
fake_session.request = request_mock
|
|
request_mock.return_value = utils.mockSessionResponse(
|
|
headers={'content-type': 'application/json'},
|
|
status_code=http_client.NO_CONTENT)
|
|
client = _session_client(session=fake_session,
|
|
endpoint_override=endpoint)
|
|
client.json_request('DELETE', '/v1/nodes/aa/maintenance')
|
|
expected_args_dict = {
|
|
'headers': {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-OpenStack-Ironic-API-Version': '1.6'
|
|
},
|
|
'auth': None, 'user_agent': 'python-ironicclient',
|
|
'endpoint_filter': {
|
|
'interface': 'publicURL',
|
|
'service_type': 'baremetal',
|
|
'region_name': ''
|
|
}
|
|
}
|
|
if isinstance(endpoint, six.string_types):
|
|
trimmed = http._trim_endpoint_api_version(endpoint)
|
|
expected_args_dict['endpoint_override'] = trimmed
|
|
request_mock.assert_called_once_with(
|
|
'/v1/nodes/aa/maintenance', 'DELETE', raise_exc=False,
|
|
**expected_args_dict
|
|
)
|
|
|
|
def test_endpoint_override(self):
|
|
self._test_endpoint_override('http://1.0.0.1:6385')
|
|
|
|
def test_endpoint_override_with_version(self):
|
|
self._test_endpoint_override('http://1.0.0.1:6385/v1')
|
|
|
|
def test_endpoint_override_not_valid(self):
|
|
self._test_endpoint_override(True)
|
|
|
|
def test_make_simple_request(self):
|
|
session = mock.Mock(spec=['request'])
|
|
|
|
client = _session_client(session=session,
|
|
endpoint_override='http://127.0.0.1')
|
|
res = client._make_simple_request(session, 'GET', 'url')
|
|
|
|
session.request.assert_called_once_with(
|
|
'url', 'GET', raise_exc=False,
|
|
endpoint_filter={
|
|
'interface': 'publicURL',
|
|
'service_type': 'baremetal',
|
|
'region_name': ''
|
|
},
|
|
user_agent=http.USER_AGENT)
|
|
self.assertEqual(res, session.request.return_value)
|
|
|
|
|
|
@mock.patch.object(time, 'sleep', lambda *_: None)
|
|
class RetriesTestCase(utils.BaseTestCase):
|
|
|
|
def test_http_no_retry(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.CONFLICT)
|
|
client = http.HTTPClient('http://localhost/', max_retries=0)
|
|
|
|
with mock.patch.object(client.session, 'request', autospec=True,
|
|
return_value=bad_resp) as mock_request:
|
|
|
|
self.assertRaises(exc.Conflict, client._http_request,
|
|
'/v1/resources', 'GET')
|
|
self.assertEqual(1, mock_request.call_count)
|
|
|
|
def test_http_retry(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.CONFLICT)
|
|
good_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'text/plain'},
|
|
"meow",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
client = http.HTTPClient('http://localhost/')
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
|
|
mock_session.request.side_effect = iter([bad_resp, good_resp])
|
|
response, body_iter = client._http_request('/v1/resources', 'GET')
|
|
|
|
self.assertEqual(http_client.OK, response.status_code)
|
|
self.assertEqual(2, mock_session.request.call_count)
|
|
|
|
def test_http_retry_503(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.SERVICE_UNAVAILABLE)
|
|
good_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'text/plain'},
|
|
"meow",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
client = http.HTTPClient('http://localhost/')
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
mock_session.request.side_effect = iter([bad_resp, good_resp])
|
|
response, body_iter = client._http_request('/v1/resources', 'GET')
|
|
|
|
self.assertEqual(http_client.OK, response.status_code)
|
|
self.assertEqual(2, mock_session.request.call_count)
|
|
|
|
def test_http_retry_connection_refused(self):
|
|
good_resp = utils.mockSessionResponse(
|
|
{'content-type': 'text/plain'},
|
|
"meow",
|
|
version=1,
|
|
status_code=http_client.OK)
|
|
client = http.HTTPClient('http://localhost/')
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
mock_session.request.side_effect = iter([exc.ConnectionRefused(),
|
|
good_resp])
|
|
response, body_iter = client._http_request('/v1/resources', 'GET')
|
|
|
|
self.assertEqual(http_client.OK, response.status_code)
|
|
self.assertEqual(2, mock_session.request.call_count)
|
|
|
|
def test_http_failed_retry(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'content-type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.CONFLICT)
|
|
client = http.HTTPClient('http://localhost/')
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
mock_session.request.return_value = bad_resp
|
|
self.assertRaises(exc.Conflict, client._http_request,
|
|
'/v1/resources', 'GET')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 1,
|
|
mock_session.request.call_count)
|
|
|
|
def test_http_max_retries_none(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'content-type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.CONFLICT)
|
|
client = http.HTTPClient('http://localhost/', max_retries=None)
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
mock_session.request.return_value = bad_resp
|
|
self.assertRaises(exc.Conflict, client._http_request,
|
|
'/v1/resources', 'GET')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 1,
|
|
mock_session.request.call_count)
|
|
|
|
def test_http_change_max_retries(self):
|
|
error_body = _get_error_body()
|
|
bad_resp = utils.mockSessionResponse(
|
|
{'content-type': 'text/plain'},
|
|
error_body,
|
|
version=1,
|
|
status_code=http_client.CONFLICT)
|
|
client = http.HTTPClient('http://localhost/',
|
|
max_retries=http.DEFAULT_MAX_RETRIES + 1)
|
|
|
|
with mock.patch.object(client, 'session',
|
|
autospec=True) as mock_session:
|
|
mock_session.request.return_value = bad_resp
|
|
self.assertRaises(exc.Conflict, client._http_request,
|
|
'/v1/resources', 'GET')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 2,
|
|
mock_session.request.call_count)
|
|
|
|
def test_session_retry(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.CONFLICT)
|
|
ok_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
b"OK",
|
|
http_client.OK)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.side_effect = iter((fake_resp, ok_resp))
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.json_request('GET', '/v1/resources')
|
|
self.assertEqual(2, fake_session.request.call_count)
|
|
|
|
def test_session_retry_503(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.SERVICE_UNAVAILABLE)
|
|
ok_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
b"OK",
|
|
http_client.OK)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.side_effect = iter((fake_resp, ok_resp))
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.json_request('GET', '/v1/resources')
|
|
self.assertEqual(2, fake_session.request.call_count)
|
|
|
|
def test_session_retry_connection_refused(self):
|
|
ok_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
b"OK",
|
|
http_client.OK)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.side_effect = iter((exc.ConnectionRefused(),
|
|
ok_resp))
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.json_request('GET', '/v1/resources')
|
|
self.assertEqual(2, fake_session.request.call_count)
|
|
|
|
def test_session_retry_retriable_connection_failure(self):
|
|
ok_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
b"OK",
|
|
http_client.OK)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.side_effect = iter(
|
|
(kexc.RetriableConnectionFailure(), ok_resp))
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.json_request('GET', '/v1/resources')
|
|
self.assertEqual(2, fake_session.request.call_count)
|
|
|
|
def test_session_retry_fail(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.CONFLICT)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.return_value = fake_resp
|
|
|
|
client = _session_client(session=fake_session)
|
|
|
|
self.assertRaises(exc.Conflict, client.json_request,
|
|
'GET', '/v1/resources')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 1,
|
|
fake_session.request.call_count)
|
|
|
|
def test_session_max_retries_none(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.CONFLICT)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.return_value = fake_resp
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.conflict_max_retries = None
|
|
|
|
self.assertRaises(exc.Conflict, client.json_request,
|
|
'GET', '/v1/resources')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 1,
|
|
fake_session.request.call_count)
|
|
|
|
def test_session_change_max_retries(self):
|
|
error_body = _get_error_body()
|
|
|
|
fake_resp = utils.mockSessionResponse(
|
|
{'Content-Type': 'application/json'},
|
|
error_body,
|
|
http_client.CONFLICT)
|
|
fake_session = mock.Mock(spec=requests.Session)
|
|
fake_session.request.return_value = fake_resp
|
|
|
|
client = _session_client(session=fake_session)
|
|
client.conflict_max_retries = http.DEFAULT_MAX_RETRIES + 1
|
|
|
|
self.assertRaises(exc.Conflict, client.json_request,
|
|
'GET', '/v1/resources')
|
|
self.assertEqual(http.DEFAULT_MAX_RETRIES + 2,
|
|
fake_session.request.call_count)
|