diff --git a/nova/limit/utils.py b/nova/limit/utils.py index a82925a2dfdb..50510932f4fc 100644 --- a/nova/limit/utils.py +++ b/nova/limit/utils.py @@ -12,33 +12,80 @@ # License for the specific language governing permissions and limitations # under the License. +import typing as ty + +if ty.TYPE_CHECKING: + from openstack import proxy + from oslo_limit import exception as limit_exceptions -from oslo_limit import limit from oslo_log import log as logging import nova.conf +from nova import utils as nova_utils LOG = logging.getLogger(__name__) CONF = nova.conf.CONF UNIFIED_LIMITS_DRIVER = "nova.quota.UnifiedLimitsDriver" -ENDPOINT = None +IDENTITY_CLIENT = None def use_unified_limits(): return CONF.quota.driver == UNIFIED_LIMITS_DRIVER -def _endpoint(): - global ENDPOINT - if ENDPOINT is None: - # This is copied from oslo_limit/limit.py - endpoint_id = CONF.oslo_limit.endpoint_id - if not endpoint_id: - raise ValueError("endpoint_id is not configured") - enforcer = limit.Enforcer(lambda: None) - ENDPOINT = enforcer.connection.get_endpoint(endpoint_id) - return ENDPOINT +class IdentityClient: + connection: 'proxy.Proxy' + service_id: str + region_id: str + + def __init__(self, connection, service_id, region_id): + self.connection = connection + self.service_id = service_id + self.region_id = region_id + + def registered_limits(self): + return list(self.connection.registered_limits( + service_id=self.service_id, region_id=self.region_id)) + + +def _identity_client(): + global IDENTITY_CLIENT + if not IDENTITY_CLIENT: + connection = nova_utils.get_sdk_adapter( + 'identity', True, conf_group='oslo_limit') + service_id = None + region_id = None + # Prefer the endpoint_id if present, same as oslo.limit. + if CONF.oslo_limit.endpoint_id is not None: + endpoint = connection.get_endpoint(CONF.oslo_limit.endpoint_id) + service_id = endpoint.service_id + region_id = endpoint.region_id + elif 'endpoint_service_type' in CONF.oslo_limit: + # This must be oslo.limit >= 2.6.0 and this block is more or less + # copied from there. + if (not CONF.oslo_limit.endpoint_service_type and not + CONF.oslo_limit.endpoint_service_name): + raise ValueError( + 'Either endpoint_service_type or endpoint_service_name ' + 'must be set') + # Get the service_id for registered limits calls. + services = connection.services( + type=CONF.oslo_limit.endpoint_service_type, + name=CONF.oslo_limit.endpoint_service_name) + if len(services) > 1: + raise ValueError('Multiple services found') + service_id = services[0].id + # Get the region_id if region name is configured. + # endpoint_region_name was added in oslo.limit 2.6.0. + if CONF.oslo_limit.endpoint_region_name: + regions = connection.regions( + name=CONF.oslo_limit.endpoint_region_name) + if len(regions) > 1: + raise ValueError('Multiple regions found') + region_id = regions[0].id + IDENTITY_CLIENT = IdentityClient(connection, service_id, region_id) + return IDENTITY_CLIENT def should_enforce(exc: limit_exceptions.ProjectOverLimit) -> bool: @@ -95,9 +142,7 @@ def should_enforce(exc: limit_exceptions.ProjectOverLimit) -> bool: # resource names however this will do one API call whereas the alternative # is calling GET /registered_limits/{registered_limit_id} for each resource # name. - enforcer = limit.Enforcer(lambda: None) - registered_limits = list(enforcer.connection.registered_limits( - service_id=_endpoint().service_id, region_id=_endpoint().region_id)) + registered_limits = _identity_client().registered_limits() # Make a set of resource names of the registered limits. have_limits_set = {limit.resource_name for limit in registered_limits} diff --git a/nova/test.py b/nova/test.py index 0ddd928904c2..8160c21fa87c 100644 --- a/nova/test.py +++ b/nova/test.py @@ -329,8 +329,8 @@ class TestCase(base.BaseTestCase): # Reset the global key manager nova.crypto._KEYMGR = None - # Reset the global endpoint - nova.limit.utils.ENDPOINT = None + # Reset the global identity client + nova.limit.utils.IDENTITY_CLIENT = None def _setup_cells(self): """Setup a normal cellsv2 environment. diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py index 8ea24f78907b..d996e325770a 100644 --- a/nova/tests/fixtures/nova.py +++ b/nova/tests/fixtures/nova.py @@ -2110,6 +2110,8 @@ class UnifiedLimitsFixture(fixtures.Fixture): 'model': {'name': 'flat'}} self.mock_sdk_adapter.get_endpoint.return_value.service_id = None self.mock_sdk_adapter.get_endpoint.return_value.region_id = None + self.mock_sdk_adapter.endpoints.return_value = [ + mock.Mock(service_id=None, region_id=None)] # These are Keystone API calls that oslo.limit will also use. self.mock_sdk_adapter.registered_limits.side_effect = ( @@ -2119,6 +2121,10 @@ class UnifiedLimitsFixture(fixtures.Fixture): self.create_registered_limit) self.mock_sdk_adapter.create_limit.side_effect = self.create_limit + # These are calls made for service endpoint discovery in limit/utils.py + self.mock_sdk_adapter.services.return_value = [mock.Mock(id=None)] + self.mock_sdk_adapter.regions.return_value = [mock.Mock(id=None)] + self.registered_limits_list = [] self.limits_list = [] diff --git a/nova/tests/functional/test_unified_limits.py b/nova/tests/functional/test_unified_limits.py index eec4991c3efc..5f935e559a81 100644 --- a/nova/tests/functional/test_unified_limits.py +++ b/nova/tests/functional/test_unified_limits.py @@ -16,6 +16,7 @@ from oslo_limit import fixture as limit_fixture from oslo_serialization import base64 from oslo_utils.fixture import uuidsentinel as uuids +import nova.conf from nova import context as nova_context from nova.limit import local as local_limit from nova.objects import flavor as flavor_obj @@ -24,6 +25,8 @@ from nova.tests import fixtures as nova_fixtures from nova.tests.functional.api import client from nova.tests.functional import integrated_helpers +CONF = nova.conf.CONF + class UnifiedLimitsTest(integrated_helpers._IntegratedTestBase): @@ -432,3 +435,45 @@ class ResourceStrategyTest(integrated_helpers._IntegratedTestBase): client.OpenStackApiException, self._create_server, api=self.admin_api) self.assertEqual(403, e.response.status_code) + + +class EndpointDiscoveryTest(UnifiedLimitsTest): + + def setUp(self): + super().setUp() + if 'endpoint_service_type' not in CONF.oslo_limit: + self.skipTest( + 'oslo.limit < 2.6.0, skipping endpoint discovery tests') + # endpoint_id has a default value in the ConfFixture but we want it to + # be None so that we do endpoint discovery. + self.flags(endpoint_id=None, group='oslo_limit') + self.flags(endpoint_service_type='compute', group='oslo_limit') + self.flags(endpoint_service_name='nova', group='oslo_limit') + + def test_endpoint_service_type_and_name_not_set(self): + self.flags(endpoint_service_type=None, group='oslo_limit') + self.flags(endpoint_service_name=None, group='oslo_limit') + e = self.assertRaises( + client.OpenStackApiException, self._create_server, api=self.api) + self.assertEqual(500, e.response.status_code) + + def test_endpoint_service_type_set(self): + self.flags(endpoint_service_type='compute', group='oslo_limit') + self.flags(endpoint_service_name=None, group='oslo_limit') + self._create_server() + + def test_endpoint_service_name_set(self): + self.flags(endpoint_service_type=None, group='oslo_limit') + self.flags(endpoint_service_name='nova', group='oslo_limit') + self._create_server() + + def test_endpoint_service_type_and_name_set(self): + self.flags(endpoint_service_type='compute', group='oslo_limit') + self.flags(endpoint_service_name='nova', group='oslo_limit') + self._create_server() + + def test_endpoint_region_name_set(self): + self.flags(endpoint_service_type='compute', group='oslo_limit') + self.flags(endpoint_service_name='nova', group='oslo_limit') + self.flags(endpoint_region_name='somewhere', group='oslo_limit') + self._create_server()