diff --git a/neutron_lib/placement/client.py b/neutron_lib/placement/client.py index fed0e3f2e..d41589e34 100644 --- a/neutron_lib/placement/client.py +++ b/neutron_lib/placement/client.py @@ -15,10 +15,14 @@ import functools import re +import uuid + +import requests from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import loading as keystone from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import versionutils from six.moves.urllib.parse import urlencode @@ -67,6 +71,64 @@ def _get_version(openstack_api_version): return versionutils.convert_version_to_tuple(match.group('api_version')) +class UUIDEncoder(jsonutils.JSONEncoder): + def default(self, o): + if isinstance(o, uuid.UUID): + return str(o) + return super(UUIDEncoder, self).default(o) + + +class NoAuthClient(object): + """Placement NoAuthClient for fullstack testing""" + + def __init__(self, url): + self.url = url + # TODO(lajoskatona): use perhaps http_connect_timeout from + # keystone_authtoken group + self.timeout = 5 + + def request(self, url, method, body=None, headers=None, **kwargs): + headers = headers or {} + headers.setdefault('Accept', 'application/json') + + # Note(lajoskatona): placement report plugin fills uuid fields with + # UUID objects, and that is good for keystone, but requests goes mad + # with that if we use json=body as it can't serialize UUID. + # To make things worse if we give a serialized json, it will do the + # jsonification again, so better to create the json here and give it + # to requests with the data parameter. + body = jsonutils.dumps(body, cls=UUIDEncoder) + try: + resp = requests.request( + method, + url, + data=body, + headers=headers, + verify=False, + timeout=self.timeout, + **kwargs) + return resp + # Note(lajoskatona): requests raise ConnectionError, but + # PlacementReportPlugin expects keystonauth1 HttpError. + except requests.ConnectionError: + raise ks_exc.HttpError + + def get(self, url, endpoint_filter, **kwargs): + return self.request('%s%s' % (self.url, url), 'GET', **kwargs) + + def post(self, url, json, endpoint_filter, **kwargs): + return self.request('%s%s' % (self.url, url), 'POST', body=json, + **kwargs) + + def put(self, url, json, endpoint_filter, **kwargs): + resp = self.request('%s%s' % (self.url, url), 'PUT', body=json, + **kwargs) + return resp + + def delete(self, url, endpoint_filter, **kwargs): + return self.request('%s%s' % (self.url, url), 'DELETE', **kwargs) + + class PlacementAPIClient(object): """Client class for placement ReST API.""" @@ -87,11 +149,18 @@ class PlacementAPIClient(object): # clean slate. self._resource_providers = {} self._provider_aggregate_map = {} - auth_plugin = keystone.load_auth_from_conf_options( - self._conf, 'placement') - return keystone.load_session_from_conf_options( - self._conf, 'placement', auth=auth_plugin, - additional_headers={'accept': 'application/json'}) + # TODO(lajoskatona): perhaps not the best to override config options, + # actually the abused keystoneauth1 options are: + # auth_type (used for deciding for NoAuthClient) and auth_section + # (used for communicating the url for the NoAuthClient) + if self._conf.placement.auth_type == 'noauth': + return NoAuthClient(self._conf.placement.auth_section) + else: + auth_plugin = keystone.load_auth_from_conf_options( + self._conf, 'placement') + return keystone.load_session_from_conf_options( + self._conf, 'placement', auth=auth_plugin, + additional_headers={'accept': 'application/json'}) def _extend_header_with_api_version(self, **kwargs): headers = kwargs.get('headers', {}) diff --git a/neutron_lib/tests/unit/placement/test_client.py b/neutron_lib/tests/unit/placement/test_client.py index 508ec0e64..2fdbd6474 100644 --- a/neutron_lib/tests/unit/placement/test_client.py +++ b/neutron_lib/tests/unit/placement/test_client.py @@ -14,6 +14,8 @@ import mock from keystoneauth1 import exceptions as ks_exc +from keystoneauth1 import loading as keystone +from oslo_serialization import jsonutils from oslo_utils import uuidutils from neutron_lib._i18n import _ @@ -39,6 +41,65 @@ INVENTORY = { } +class TestNoAuthClient(base.BaseTestCase): + + def setUp(self): + super(TestNoAuthClient, self).setUp() + self.noauth_client = place_client.NoAuthClient('placement/') + self.body_json = jsonutils.dumps({'name': 'foo'}) + self.uuid = '42' + + @mock.patch.object(place_client.NoAuthClient, 'request') + def test_get(self, mock_request): + self.noauth_client.get('resource_providers', '') + mock_request.assert_called_with('placement/resource_providers', 'GET') + + @mock.patch.object(place_client.NoAuthClient, 'request') + def test_post(self, mock_request): + self.noauth_client.post('resource_providers', self.body_json, '') + mock_request.assert_called_with('placement/resource_providers', 'POST', + body=self.body_json) + + @mock.patch.object(place_client.NoAuthClient, 'request') + def test_put(self, mock_request): + self.noauth_client.put('resource_providers/%s' % self.uuid, + self.body_json, '') + mock_request.assert_called_with( + 'placement/resource_providers/%s' % self.uuid, 'PUT', + body=self.body_json) + + @mock.patch.object(place_client.NoAuthClient, 'request') + def test_delete(self, mock_request): + self.noauth_client.delete('resource_providers/%s' % self.uuid, '') + mock_request.assert_called_with( + 'placement/resource_providers/%s' % self.uuid, 'DELETE') + + +class TestPlacementAPIClientNoAuth(base.BaseTestCase): + def setUp(self): + super(TestPlacementAPIClientNoAuth, self).setUp() + self.config = mock.Mock() + + @mock.patch('neutron_lib.placement.client.NoAuthClient', autospec=True) + def test__create_client_noauth(self, mock_auth_client): + self.config.placement.auth_type = 'noauth' + self.config.placement.auth_section = 'placement/' + self.placement_api_client = place_client.PlacementAPIClient( + self.config) + self.placement_api_client._create_client() + mock_auth_client.assert_called_once_with('placement/') + + @mock.patch.object(keystone, 'load_auth_from_conf_options') + @mock.patch.object(keystone, 'load_session_from_conf_options') + def test__create_client(self, mock_session_from_conf, mock_auth_from_conf): + self.config.placement.auth_type = 'password' + self.placement_api_client = place_client.PlacementAPIClient( + self.config) + self.placement_api_client._create_client() + mock_auth_from_conf.assert_called_once_with(self.config, 'placement') + mock_session_from_conf.assert_called_once() + + class TestPlacementAPIClient(base.BaseTestCase): def setUp(self): diff --git a/releasenotes/notes/placement-NoAuthClient-for-fullstack-tests-17b4ab512417d638.yaml b/releasenotes/notes/placement-NoAuthClient-for-fullstack-tests-17b4ab512417d638.yaml new file mode 100644 index 000000000..7e37b4b62 --- /dev/null +++ b/releasenotes/notes/placement-NoAuthClient-for-fullstack-tests-17b4ab512417d638.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + Add ``NoAuthClient`` for placement.client to enable fullstack testing of + placement reporting service plugin.