unified limits: discover service ID and region ID

In oslo.limit 2.6.0 service endpoint discovery was added, provided by
three new config options:

  [oslo_limit]
  endpoint_service_type = ...
  endpoint_service_name = ...
  endpoint_region_name = ...

We can use the same config options if they are present to lookup the
service ID and region ID we need when calling the
GET /registered_limits API as part of the resource limit enforcement
strategy. This way, the user will not have to configure endpoint_id.

This will look for [oslo.limit]endpoint_id first and if it is not set,
it will do the discovery.

Closes-Bug: #1931875

Change-Id: Ida14303115e00a1460e6bef4b6d25fc68f343a4e
This commit is contained in:
melanie witt
2025-03-05 21:12:41 +00:00
parent 29d17552a7
commit eb3a803cd7
4 changed files with 113 additions and 17 deletions

View File

@@ -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}

View File

@@ -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.

View File

@@ -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 = []

View File

@@ -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()