From 987d451f4d4c8006cc6e32ea2d966c749c2d9265 Mon Sep 17 00:00:00 2001 From: Eric Fried Date: Wed, 9 Aug 2017 15:09:14 -0500 Subject: [PATCH] Use ksa adapter for placement conf & requests Switch usage of the placement API over to using the new nova.utils.get_ksa_adapter method. Now REST calls with the placement API go through the resulting keystoneauth1 Adapter - which already incorporates endpoint filtering - rather than a keystoneauth1 Session. To make this fit, switch the placement conf over to using nova.conf.utils.register_ksa_opts and get_ksa_adapter_opts. In so doing, deprecate os_interface and os_region_name in favor of the imported Adapter opts valid_interfaces and region_name, respectively. Change-Id: I69e9b30d96390a70198b12d74e7efa9bd61db217 Partial-Implements: bp use-ksa-adapter-for-endpoints --- nova/cmd/status.py | 17 +- nova/conf/placement.py | 40 ++- nova/scheduler/client/report.py | 65 +---- .../openstack/placement/test_report_client.py | 10 +- nova/tests/unit/cmd/test_status.py | 25 +- .../unit/scheduler/client/test_report.py | 254 ++++++++---------- nova/utils.py | 10 +- .../placement-via-ksa-02d87c87636912f8.yaml | 16 ++ 8 files changed, 203 insertions(+), 234 deletions(-) create mode 100644 releasenotes/notes/placement-via-ksa-02d87c87636912f8.yaml diff --git a/nova/cmd/status.py b/nova/cmd/status.py index 7720aad330ca..34b718533ec7 100644 --- a/nova/cmd/status.py +++ b/nova/cmd/status.py @@ -26,7 +26,6 @@ import textwrap import traceback from keystoneauth1 import exceptions as ks_exc -from keystoneauth1 import loading as keystone from oslo_config import cfg import pkg_resources import prettytable @@ -41,6 +40,7 @@ from nova.db.sqlalchemy import api as db_session from nova.i18n import _ from nova.objects import cell_mapping as cell_mapping_obj from nova.objects import fields +from nova import utils from nova import version CONF = nova.conf.CONF @@ -174,22 +174,16 @@ class UpgradeCommands(object): return UpgradeCheckResult(UpgradeCheckCode.SUCCESS) - def _placement_get(self, path): + @staticmethod + def _placement_get(path): """Do an HTTP get call against placement engine. This is in a dedicated method to make it easier for unit testing purposes. """ - ks_filter = {'service_type': 'placement', - 'region_name': CONF.placement.os_region_name, - 'interface': CONF.placement.os_interface} - auth = keystone.load_auth_from_conf_options( - CONF, 'placement') - client = keystone.load_session_from_conf_options( - CONF, 'placement', auth=auth) - - return client.get(path, endpoint_filter=ks_filter).json() + client = utils.get_ksa_adapter('placement') + return client.get(path).json() def _check_placement(self): """Checks to see if the placement API is ready for scheduling. @@ -198,6 +192,7 @@ class UpgradeCommands(object): service catalog and that we can make requests against it. """ try: + # TODO(efried): Use ksa's version filtering in _placement_get versions = self._placement_get("/") max_version = pkg_resources.parse_version( versions["versions"][0]["max_version"]) diff --git a/nova/conf/placement.py b/nova/conf/placement.py index 14754bac5691..69c920a5df46 100644 --- a/nova/conf/placement.py +++ b/nova/conf/placement.py @@ -13,14 +13,25 @@ from keystoneauth1 import loading as ks_loading from oslo_config import cfg +from nova.conf import utils as confutils + + +DEFAULT_SERVICE_TYPE = 'placement' + placement_group = cfg.OptGroup( 'placement', title='Placement Service Options', help="Configuration options for connecting to the placement API service") placement_opts = [ - cfg.StrOpt('os_region_name', - help=""" + cfg.StrOpt( + 'os_region_name', + deprecated_for_removal=True, + deprecated_since='17.0.0', + deprecated_reason='Endpoint lookup uses the service catalog via ' + 'common keystoneauth1 Adapter configuration ' + 'options. Use the region_name option instead.', + help=""" Region name of this node. This is used when picking the URL in the service catalog. @@ -28,19 +39,32 @@ Possible values: * Any string representing region name """), - cfg.StrOpt('os_interface', - help=""" + cfg.StrOpt( + 'os_interface', + deprecated_for_removal=True, + deprecated_since='17.0.0', + deprecated_reason='Endpoint lookup uses the service catalog via ' + 'common keystoneauth1 Adapter configuration ' + 'options. Use the valid_interfaces option instead.', + help=""" Endpoint interface for this node. This is used when picking the URL in the service catalog. """) ] +deprecated_opts = { + 'region_name': [cfg.DeprecatedOpt('os_region_name', + group=placement_group.name)], + 'valid_interfaces': [cfg.DeprecatedOpt('os_interface', + group=placement_group.name)] +} + def register_opts(conf): conf.register_group(placement_group) conf.register_opts(placement_opts, group=placement_group) - ks_loading.register_session_conf_options(conf, placement_group.name) - ks_loading.register_auth_conf_options(conf, placement_group.name) + confutils.register_ksa_opts(conf, placement_group, DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_opts) def list_opts(): @@ -51,5 +75,7 @@ def list_opts(): ks_loading.get_auth_common_conf_options() + ks_loading.get_auth_plugin_conf_options('password') + ks_loading.get_auth_plugin_conf_options('v2password') + - ks_loading.get_auth_plugin_conf_options('v3password')) + ks_loading.get_auth_plugin_conf_options('v3password') + + confutils.get_ksa_adapter_opts(DEFAULT_SERVICE_TYPE, + deprecated_opts=deprecated_opts)) } diff --git a/nova/scheduler/client/report.py b/nova/scheduler/client/report.py index 1fa52ef7e6c7..63dc25d4a516 100644 --- a/nova/scheduler/client/report.py +++ b/nova/scheduler/client/report.py @@ -19,7 +19,6 @@ import re import time from keystoneauth1 import exceptions as ks_exc -from keystoneauth1 import loading as keystone from oslo_log import log as logging from six.moves.urllib import parse @@ -246,9 +245,6 @@ class SchedulerReportClient(object): self._client = self._create_client() # NOTE(danms): Keep track of how naggy we've been self._warn_count = 0 - self.ks_filter = {'service_type': 'placement', - 'region_name': CONF.placement.os_region_name, - 'interface': CONF.placement.os_interface} @utils.synchronized(PLACEMENT_CLIENT_SEMAPHORE) def _create_client(self): @@ -257,73 +253,36 @@ class SchedulerReportClient(object): self._provider_tree = provider_tree.ProviderTree() self._provider_aggregate_map = {} self.aggregate_refresh_time = {} - auth_plugin = keystone.load_auth_from_conf_options( - CONF, 'placement') - return keystone.load_session_from_conf_options( - CONF, 'placement', auth=auth_plugin, - additional_headers={'accept': 'application/json'}) + # TODO(mriedem): Perform some version discovery at some point. + client = utils.get_ksa_adapter('placement') + # Set accept header on every request to ensure we notify placement + # service of our response body media type preferences. + client.additional_headers = {'accept': 'application/json'} + return client def get(self, url, version=None): - kwargs = {} - if version is not None: - # TODO(mriedem): Perform some version discovery at some point. - kwargs = { - 'headers': { - 'OpenStack-API-Version': 'placement %s' % version - }, - } - return self._client.get( - url, - endpoint_filter=self.ks_filter, raise_exc=False, **kwargs) + return self._client.get(url, raise_exc=False, microversion=version) def post(self, url, data, version=None): # NOTE(sdague): using json= instead of data= sets the # media type to application/json for us. Placement API is # more sensitive to this than other APIs in the OpenStack # ecosystem. - kwargs = {} - if version is not None: - # TODO(mriedem): Perform some version discovery at some point. - kwargs = { - 'headers': { - 'OpenStack-API-Version': 'placement %s' % version - }, - } - return self._client.post( - url, json=data, - endpoint_filter=self.ks_filter, raise_exc=False, **kwargs) + return self._client.post(url, json=data, raise_exc=False, + microversion=version) def put(self, url, data, version=None): # NOTE(sdague): using json= instead of data= sets the # media type to application/json for us. Placement API is # more sensitive to this than other APIs in the OpenStack # ecosystem. - kwargs = {} - if version is not None: - # TODO(mriedem): Perform some version discovery at some point. - kwargs = { - 'headers': { - 'OpenStack-API-Version': 'placement %s' % version - }, - } + kwargs = {'microversion': version} if data: kwargs['json'] = data - return self._client.put( - url, endpoint_filter=self.ks_filter, raise_exc=False, - **kwargs) + return self._client.put(url, raise_exc=False, **kwargs) def delete(self, url, version=None): - kwargs = {} - if version is not None: - # TODO(mriedem): Perform some version discovery at some point. - kwargs = { - 'headers': { - 'OpenStack-API-Version': 'placement %s' % version - }, - } - return self._client.delete( - url, - endpoint_filter=self.ks_filter, raise_exc=False, **kwargs) + return self._client.delete(url, raise_exc=False, microversion=version) @safe_connect def get_allocation_candidates(self, resources): diff --git a/nova/tests/functional/api/openstack/placement/test_report_client.py b/nova/tests/functional/api/openstack/placement/test_report_client.py index 963d3fba7541..bfb9858be0c6 100644 --- a/nova/tests/functional/api/openstack/placement/test_report_client.py +++ b/nova/tests/functional/api/openstack/placement/test_report_client.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystoneauth1 import adapter from keystoneauth1 import session import mock import requests @@ -41,11 +42,10 @@ class NoAuthReportClient(report.SchedulerReportClient): 'x-auth-token': 'admin', 'OpenStack-API-Version': 'placement latest', } - self._client = session.Session( - auth=None, - session=request_session, - additional_headers=headers, - ) + self._client = adapter.Adapter( + session.Session(auth=None, session=request_session, + additional_headers=headers), + service_type='placement') class SchedulerReportClientTests(test.TestCase): diff --git a/nova/tests/unit/cmd/test_status.py b/nova/tests/unit/cmd/test_status.py index a5fc0ede1a65..711baef820a5 100644 --- a/nova/tests/unit/cmd/test_status.py +++ b/nova/tests/unit/cmd/test_status.py @@ -123,34 +123,33 @@ class TestPlacementCheck(test.NoDBTestCase): self.assertIn('No credentials specified', res.details) @mock.patch.object(keystone, "load_auth_from_conf_options") - @mock.patch.object(session.Session, 'get') + @mock.patch.object(session.Session, 'request') def _test_placement_get_interface( self, expected_interface, mock_get, mock_auth): - def fake_get(path, *a, **kw): + def fake_request(path, method, *a, **kw): self.assertEqual(mock.sentinel.path, path) + self.assertEqual('GET', method) self.assertIn('endpoint_filter', kw) self.assertEqual(expected_interface, kw['endpoint_filter']['interface']) return mock.Mock(autospec='requests.models.Response') - mock_get.side_effect = fake_get + mock_get.side_effect = fake_request self.cmd._placement_get(mock.sentinel.path) mock_auth.assert_called_once_with(status.CONF, 'placement') self.assertTrue(mock_get.called) - @mock.patch.object(keystone, "load_auth_from_conf_options") - @mock.patch.object(session.Session, 'get') - def test_placement_get_interface_default(self, mock_get, mock_auth): - """Tests that None is specified for interface by default.""" - self._test_placement_get_interface(None) + def test_placement_get_interface_default(self): + """Tests that we try internal, then public interface by default.""" + self._test_placement_get_interface(['internal', 'public']) - @mock.patch.object(keystone, "load_auth_from_conf_options") - @mock.patch.object(session.Session, 'get') - def test_placement_get_interface_internal(self, mock_get, mock_auth): + def test_placement_get_interface_internal(self): """Tests that "internal" is specified for interface when configured.""" - self.flags(os_interface='internal', group='placement') - self._test_placement_get_interface('internal') + # TODO(efried): Test that the deprecated opts (e.g. os_interface) still + # work once bug #1709728 is resolved. + self.flags(valid_interfaces='internal', group='placement') + self._test_placement_get_interface(['internal']) @mock.patch.object(status.UpgradeCommands, "_placement_get") def test_invalid_auth(self, get): diff --git a/nova/tests/unit/scheduler/client/test_report.py b/nova/tests/unit/scheduler/client/test_report.py index 83c44b7339a2..06dc841fc5b0 100644 --- a/nova/tests/unit/scheduler/client/test_report.py +++ b/nova/tests/unit/scheduler/client/test_report.py @@ -36,11 +36,8 @@ class SafeConnectedTestCase(test.NoDBTestCase): def setUp(self): super(SafeConnectedTestCase, self).setUp() self.context = context.get_admin_context() - self.ks_sess_mock = mock.Mock() - with test.nested( - mock.patch('keystoneauth1.loading.load_auth_from_conf_options') - ) as _auth_mock: # noqa + with mock.patch('keystoneauth1.loading.load_auth_from_conf_options'): self.client = report.SchedulerReportClient() @mock.patch('keystoneauth1.session.Session.request') @@ -157,21 +154,23 @@ class TestConstructor(test.NoDBTestCase): load_auth_mock.assert_called_once_with(CONF, 'placement') load_sess_mock.assert_called_once_with(CONF, 'placement', - additional_headers={'accept': 'application/json'}, - auth=load_auth_mock.return_value) - self.assertIsNone(client.ks_filter['interface']) + auth=load_auth_mock.return_value) + self.assertEqual(['internal', 'public'], client._client.interface) + self.assertEqual({'accept': 'application/json'}, + client._client.additional_headers) @mock.patch('keystoneauth1.loading.load_session_from_conf_options') @mock.patch('keystoneauth1.loading.load_auth_from_conf_options') def test_constructor_admin_interface(self, load_auth_mock, load_sess_mock): - self.flags(os_interface='admin', group='placement') + self.flags(valid_interfaces='admin', group='placement') client = report.SchedulerReportClient() load_auth_mock.assert_called_once_with(CONF, 'placement') load_sess_mock.assert_called_once_with(CONF, 'placement', - additional_headers={'accept': 'application/json'}, - auth=load_auth_mock.return_value) - self.assertEqual('admin', client.ks_filter['interface']) + auth=load_auth_mock.return_value) + self.assertEqual(['admin'], client._client.interface) + self.assertEqual({'accept': 'application/json'}, + client._client.additional_headers) class SchedulerReportClientTestCase(test.NoDBTestCase): @@ -179,7 +178,7 @@ class SchedulerReportClientTestCase(test.NoDBTestCase): def setUp(self): super(SchedulerReportClientTestCase, self).setUp() self.context = context.get_admin_context() - self.ks_sess_mock = mock.Mock() + self.ks_adap_mock = mock.Mock() self.compute_node = objects.ComputeNode( uuid=uuids.compute_node, hypervisor_hostname='foo', @@ -192,10 +191,10 @@ class SchedulerReportClientTestCase(test.NoDBTestCase): ) with test.nested( - mock.patch('keystoneauth1.session.Session', - return_value=self.ks_sess_mock), + mock.patch('keystoneauth1.adapter.Adapter', + return_value=self.ks_adap_mock), mock.patch('keystoneauth1.loading.load_auth_from_conf_options') - ) as (_auth_mock, _sess_mock): + ): self.client = report.SchedulerReportClient() def _init_provider_tree(self, generation_override=None, @@ -298,9 +297,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): get_resp_mock.json.return_value = { 'allocations': {}, # build instance, not move } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid alloc_req = { 'allocations': { @@ -323,10 +322,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): expected_payload = copy.deepcopy(alloc_req) expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=expected_payload, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=expected_payload, + raise_exc=False) self.assertTrue(res) @@ -349,9 +347,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid alloc_req = { 'allocations': [ @@ -399,13 +397,12 @@ class TestPutAllocations(SchedulerReportClientTestCase): } expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, + raise_exc=False) # We have to pull the json body from the mock call_args to validate # it separately otherwise hash seed issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) @@ -443,9 +440,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid alloc_req = { 'allocations': [ @@ -510,14 +507,13 @@ class TestPutAllocations(SchedulerReportClientTestCase): } expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, + raise_exc=False) # We have to pull the allocations from the json body from the # mock call_args to validate it separately otherwise hash seed # issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) @@ -548,9 +544,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, } - self.ks_sess_mock.get.return_value = get_current_allocations_resp_mock + self.ks_adap_mock.get.return_value = get_current_allocations_resp_mock put_allocations_resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = put_allocations_resp_mock + self.ks_adap_mock.put.return_value = put_allocations_resp_mock consumer_uuid = uuids.consumer_uuid # This is the resize-up allocation where VCPU, MEMORY_MB and DISK_GB # are all being increased but on the same host. We also throw a custom @@ -597,13 +593,11 @@ class TestPutAllocations(SchedulerReportClientTestCase): } expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, raise_exc=False) # We have to pull the json body from the mock call_args to validate # it separately otherwise hash seed issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) @@ -641,9 +635,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, } - self.ks_sess_mock.get.return_value = get_current_allocations_resp_mock + self.ks_adap_mock.get.return_value = get_current_allocations_resp_mock put_allocations_resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = put_allocations_resp_mock + self.ks_adap_mock.put.return_value = put_allocations_resp_mock consumer_uuid = uuids.consumer_uuid # This is the resize-up allocation where VCPU, MEMORY_MB and DISK_GB # are all being increased but DISK_GB is on a shared storage provider. @@ -700,13 +694,11 @@ class TestPutAllocations(SchedulerReportClientTestCase): } expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, raise_exc=False) # We have to pull the json body from the mock call_args to validate # it separately otherwise hash seed issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) @@ -721,7 +713,7 @@ class TestPutAllocations(SchedulerReportClientTestCase): get_resp_mock.json.return_value = { 'allocations': {}, # build instance, not move } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mocks = [ mock.Mock( status_code=409, @@ -730,7 +722,7 @@ class TestPutAllocations(SchedulerReportClientTestCase): 'Please retry your update'), mock.Mock(status_code=204), ] - self.ks_sess_mock.put.side_effect = resp_mocks + self.ks_adap_mock.put.side_effect = resp_mocks consumer_uuid = uuids.consumer_uuid alloc_req = { 'allocations': [ @@ -758,16 +750,11 @@ class TestPutAllocations(SchedulerReportClientTestCase): # We should have exactly two calls to the placement API that look # identical since we're retrying the same HTTP request expected_calls = [ - mock.call(expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=expected_payload, raise_exc=False), - mock.call(expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=expected_payload, raise_exc=False), - ] + mock.call(expected_url, microversion='1.10', json=expected_payload, + raise_exc=False)] * 2 self.assertEqual(len(expected_calls), - self.ks_sess_mock.put.call_count) - self.ks_sess_mock.put.assert_has_calls(expected_calls) + self.ks_adap_mock.put.call_count) + self.ks_adap_mock.put.assert_has_calls(expected_calls) self.assertTrue(res) @@ -777,9 +764,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): get_resp_mock.json.return_value = { 'allocations': {}, # build instance, not move } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=409) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid alloc_req = { 'allocations': [ @@ -804,10 +791,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): expected_payload = copy.deepcopy(alloc_req) expected_payload['project_id'] = project_id expected_payload['user_id'] = user_id - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=expected_payload, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=expected_payload, + raise_exc=False) self.assertFalse(res) self.assertTrue(mock_log.called) @@ -837,9 +823,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, }, } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid project_id = uuids.project_id user_id = uuids.user_id @@ -865,17 +851,15 @@ class TestPutAllocations(SchedulerReportClientTestCase): expected_payload['user_id'] = user_id # We have to pull the json body from the mock call_args to validate # it separately otherwise hash seed issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) actual_allocations = sorted(actual_payload['allocations'], key=sort_by_uuid) self.assertEqual(expected_allocations, actual_allocations) - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, raise_exc=False) self.assertTrue(res) @@ -911,9 +895,9 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, }, } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock resp_mock = mock.Mock(status_code=204) - self.ks_sess_mock.put.return_value = resp_mock + self.ks_adap_mock.put.return_value = resp_mock consumer_uuid = uuids.consumer_uuid project_id = uuids.project_id user_id = uuids.user_id @@ -947,17 +931,15 @@ class TestPutAllocations(SchedulerReportClientTestCase): expected_payload['user_id'] = user_id # We have to pull the json body from the mock call_args to validate # it separately otherwise hash seed issues get in the way. - actual_payload = self.ks_sess_mock.put.call_args[1]['json'] + actual_payload = self.ks_adap_mock.put.call_args[1]['json'] sort_by_uuid = lambda x: x['resource_provider']['uuid'] expected_allocations = sorted(expected_payload['allocations'], key=sort_by_uuid) actual_allocations = sorted(actual_payload['allocations'], key=sort_by_uuid) self.assertEqual(expected_allocations, actual_allocations) - self.ks_sess_mock.put.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, - headers={'OpenStack-API-Version': 'placement 1.10'}, - json=mock.ANY, raise_exc=False) + self.ks_adap_mock.put.assert_called_once_with( + expected_url, microversion='1.10', json=mock.ANY, raise_exc=False) self.assertTrue(res) @@ -986,15 +968,15 @@ class TestPutAllocations(SchedulerReportClientTestCase): }, }, } - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock consumer_uuid = uuids.consumer_uuid project_id = uuids.project_id user_id = uuids.user_id res = self.client.remove_provider_from_instance_allocation( consumer_uuid, uuids.source, user_id, project_id, mock.Mock()) - self.ks_sess_mock.get.assert_called() - self.ks_sess_mock.put.assert_not_called() + self.ks_adap_mock.get.assert_called() + self.ks_adap_mock.put.assert_not_called() self.assertTrue(res) @@ -1004,15 +986,15 @@ class TestPutAllocations(SchedulerReportClientTestCase): existing allocations fails for some reason """ get_resp_mock = mock.Mock(status_code=500) - self.ks_sess_mock.get.return_value = get_resp_mock + self.ks_adap_mock.get.return_value = get_resp_mock consumer_uuid = uuids.consumer_uuid project_id = uuids.project_id user_id = uuids.user_id res = self.client.remove_provider_from_instance_allocation( consumer_uuid, uuids.source, user_id, project_id, mock.Mock()) - self.ks_sess_mock.get.assert_called() - self.ks_sess_mock.put.assert_not_called() + self.ks_adap_mock.get.assert_called() + self.ks_adap_mock.put.assert_not_called() self.assertFalse(res) @@ -1148,15 +1130,14 @@ class TestProviderOperations(SchedulerReportClientTestCase): } resources = {'VCPU': 1, 'MEMORY_MB': 1024} resp_mock.json.return_value = json_data - self.ks_sess_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value = resp_mock alloc_reqs, p_sums = self.client.get_allocation_candidates(resources) expected_url = '/allocation_candidates?%s' % parse.urlencode( {'resources': 'MEMORY_MB:1024,VCPU:1'}) - self.ks_sess_mock.get.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, raise_exc=False, - headers={'OpenStack-API-Version': 'placement 1.10'}) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion='1.10') self.assertEqual(mock.sentinel.alloc_reqs, alloc_reqs) self.assertEqual(mock.sentinel.p_sums, p_sums) @@ -1164,14 +1145,13 @@ class TestProviderOperations(SchedulerReportClientTestCase): # Ensure _get_resource_provider() just returns None when the placement # API doesn't find a resource provider matching a UUID resp_mock = mock.Mock(status_code=404) - self.ks_sess_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value = resp_mock res = self.client.get_allocation_candidates({'foo': 'bar'}) expected_url = '/allocation_candidates?resources=foo%3Abar' - self.ks_sess_mock.get.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, raise_exc=False, - headers={'OpenStack-API-Version': 'placement 1.10'}) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion='1.10') self.assertIsNone(res[0]) self.assertIsNone(res[0]) @@ -1186,7 +1166,7 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'generation': 42, } resp_mock.json.return_value = json_data - self.ks_sess_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value = resp_mock result = self.client._get_resource_provider(uuid) @@ -1196,24 +1176,22 @@ class TestProviderOperations(SchedulerReportClientTestCase): generation=42, ) expected_url = '/resource_providers/' + uuid - self.ks_sess_mock.get.assert_called_once_with(expected_url, - endpoint_filter=mock.ANY, - raise_exc=False) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion=None) self.assertEqual(expected_provider_dict, result) def test_get_resource_provider_not_found(self): # Ensure _get_resource_provider() just returns None when the placement # API doesn't find a resource provider matching a UUID resp_mock = mock.Mock(status_code=404) - self.ks_sess_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value = resp_mock uuid = uuids.compute_node result = self.client._get_resource_provider(uuid) expected_url = '/resource_providers/' + uuid - self.ks_sess_mock.get.assert_called_once_with(expected_url, - endpoint_filter=mock.ANY, - raise_exc=False) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion=None) self.assertIsNone(result) @mock.patch.object(report.LOG, 'error') @@ -1222,19 +1200,18 @@ class TestProviderOperations(SchedulerReportClientTestCase): # communicate with the placement API and not getting an error we can # deal with resp_mock = mock.Mock(status_code=503) - self.ks_sess_mock.get.return_value = resp_mock - self.ks_sess_mock.get.return_value.headers = { + self.ks_adap_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value.headers = { 'openstack-request-id': uuids.request_id} uuid = uuids.compute_node result = self.client._get_resource_provider(uuid) expected_url = '/resource_providers/' + uuid - self.ks_sess_mock.get.assert_called_once_with(expected_url, - endpoint_filter=mock.ANY, - raise_exc=False) - # A 503 Service Unavailable should trigger an error log that - # includes the placement request id and return None + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion=None) + # A 503 Service Unavailable should trigger an error log + # that includes the placement request id and return None # from _get_resource_provider() self.assertTrue(logging_mock.called) self.assertIsNone(result) @@ -1248,7 +1225,7 @@ class TestProviderOperations(SchedulerReportClientTestCase): uuid = uuids.compute_node name = 'computehost' resp_mock = mock.Mock(status_code=201) - self.ks_sess_mock.post.return_value = resp_mock + self.ks_adap_mock.post.return_value = resp_mock result = self.client._create_resource_provider(uuid, name) @@ -1262,11 +1239,9 @@ class TestProviderOperations(SchedulerReportClientTestCase): generation=0, ) expected_url = '/resource_providers' - self.ks_sess_mock.post.assert_called_once_with( - expected_url, - endpoint_filter=mock.ANY, - json=expected_payload, - raise_exc=False) + self.ks_adap_mock.post.assert_called_once_with( + expected_url, json=expected_payload, raise_exc=False, + microversion=None) self.assertEqual(expected_provider_dict, result) @mock.patch.object(report.LOG, 'info') @@ -1282,8 +1257,8 @@ class TestProviderOperations(SchedulerReportClientTestCase): uuid = uuids.compute_node name = 'computehost' resp_mock = mock.Mock(status_code=409) - self.ks_sess_mock.post.return_value = resp_mock - self.ks_sess_mock.post.return_value.headers = { + self.ks_adap_mock.post.return_value = resp_mock + self.ks_adap_mock.post.return_value.headers = { 'openstack-request-id': uuids.request_id} get_rp_mock.return_value = mock.sentinel.get_rp @@ -1295,11 +1270,9 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'name': name, } expected_url = '/resource_providers' - self.ks_sess_mock.post.assert_called_once_with( - expected_url, - endpoint_filter=mock.ANY, - json=expected_payload, - raise_exc=False) + self.ks_adap_mock.post.assert_called_once_with( + expected_url, json=expected_payload, raise_exc=False, + microversion=None) self.assertEqual(mock.sentinel.get_rp, result) # The 409 response will produce a message to the info log. self.assertTrue(logging_mock.called) @@ -1314,8 +1287,8 @@ class TestProviderOperations(SchedulerReportClientTestCase): uuid = uuids.compute_node name = 'computehost' resp_mock = mock.Mock(status_code=503) - self.ks_sess_mock.post.return_value = resp_mock - self.ks_sess_mock.post.return_value.headers = { + self.ks_adap_mock.post.return_value = resp_mock + self.ks_adap_mock.post.return_value.headers = { 'x-openstack-request-id': uuids.request_id} result = self.client._create_resource_provider(uuid, name) @@ -1325,11 +1298,9 @@ class TestProviderOperations(SchedulerReportClientTestCase): 'name': name, } expected_url = '/resource_providers' - self.ks_sess_mock.post.assert_called_once_with( - expected_url, - endpoint_filter=mock.ANY, - json=expected_payload, - raise_exc=False) + self.ks_adap_mock.post.assert_called_once_with( + expected_url, json=expected_payload, raise_exc=False, + microversion=None) # A 503 Service Unavailable should log an error that # includes the placement request id and # _create_resource_provider() should return None @@ -1353,7 +1324,7 @@ class TestAggregates(SchedulerReportClientTestCase): ], } resp_mock.json.return_value = json_data - self.ks_sess_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value = resp_mock result = self.client._get_provider_aggregates(uuid) @@ -1362,9 +1333,8 @@ class TestAggregates(SchedulerReportClientTestCase): uuids.agg2, ]) expected_url = '/resource_providers/' + uuid + '/aggregates' - self.ks_sess_mock.get.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, raise_exc=False, - headers={'OpenStack-API-Version': 'placement 1.1'}) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion='1.1') self.assertEqual(expected, result) @mock.patch.object(report.LOG, 'warning') @@ -1376,16 +1346,15 @@ class TestAggregates(SchedulerReportClientTestCase): """ uuid = uuids.compute_node resp_mock = mock.Mock(status_code=404) - self.ks_sess_mock.get.return_value = resp_mock - self.ks_sess_mock.get.return_value.headers = { + self.ks_adap_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value.headers = { 'x-openstack-request-id': uuids.request_id} result = self.client._get_provider_aggregates(uuid) expected_url = '/resource_providers/' + uuid + '/aggregates' - self.ks_sess_mock.get.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, raise_exc=False, - headers={'OpenStack-API-Version': 'placement 1.1'}) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion='1.1') self.assertTrue(log_mock.called) self.assertEqual(uuids.request_id, log_mock.call_args[0][1]['placement_req_id']) @@ -1398,16 +1367,15 @@ class TestAggregates(SchedulerReportClientTestCase): """ uuid = uuids.compute_node resp_mock = mock.Mock(status_code=400) - self.ks_sess_mock.get.return_value = resp_mock - self.ks_sess_mock.get.return_value.headers = { + self.ks_adap_mock.get.return_value = resp_mock + self.ks_adap_mock.get.return_value.headers = { 'x-openstack-request-id': uuids.request_id} result = self.client._get_provider_aggregates(uuid) expected_url = '/resource_providers/' + uuid + '/aggregates' - self.ks_sess_mock.get.assert_called_once_with( - expected_url, endpoint_filter=mock.ANY, raise_exc=False, - headers={'OpenStack-API-Version': 'placement 1.1'}) + self.ks_adap_mock.get.assert_called_once_with( + expected_url, raise_exc=False, microversion='1.1') self.assertTrue(log_mock.called) self.assertEqual(uuids.request_id, log_mock.call_args[0][1]['placement_req_id']) diff --git a/nova/utils.py b/nova/utils.py index a615599fa59b..09665b281014 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -1299,8 +1299,14 @@ def get_ksa_adapter(service_type, ksa_auth=None, ksa_session=None, """ # Get the conf group corresponding to the service type. confgrp = _SERVICE_TYPES.get_project_name(service_type) - if not confgrp: - raise exception.ConfGroupForServiceTypeNotFound(stype=service_type) + if not confgrp or not hasattr(CONF, confgrp): + # Try the service type as the conf group. This is necessary for e.g. + # placement, while it's still part of the nova project. + # Note that this might become the first thing we try if/as we move to + # using service types for conf group names in general. + confgrp = service_type + if not confgrp or not hasattr(CONF, confgrp): + raise exception.ConfGroupForServiceTypeNotFound(stype=service_type) # Ensure we have an auth. # NOTE(efried): This could be None, and that could be okay - e.g. if the diff --git a/releasenotes/notes/placement-via-ksa-02d87c87636912f8.yaml b/releasenotes/notes/placement-via-ksa-02d87c87636912f8.yaml new file mode 100644 index 000000000000..252d1cdb36ee --- /dev/null +++ b/releasenotes/notes/placement-via-ksa-02d87c87636912f8.yaml @@ -0,0 +1,16 @@ +--- +upgrade: + - | + Nova now uses keystoneauth1 configuration to set up communication with the + placement service. Use keystoneauth1 loading parameters for auth, Session, + and Adapter setup in the ``[placement]`` conf section. Note that, by + default, the 'internal' interface will be tried first, followed by the + 'public' interface. Use the conf option ``[placement].valid_interfaces`` + to override this behavior. +deprecations: + - | + Configuration options in the ``[placement]`` section are deprecated as + follows: + + * ``os_region_name`` is deprecated in favor of ``region_name`` + * ``os_interface`` is deprecated in favor of ``valid_interfaces``