From 9575a24796e019fd66f1bb1a5ef0bcbfc167351a Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sat, 30 Mar 2024 22:25:14 +0900 Subject: [PATCH] Query endpoint id from keystone Endpoint id is not predictable so users can't configure the endpoint_id option until keystone endpoints are created. This requires redundant steps in deployment. For example both keystone and glance are run by httpd + mod_wsgi then you first have to deploy keystone and then create glance endpoints, until you can install glance and restart httpd. This introduces a few new options to look up the target endpoint from Keystone. All these options accept predictable values. Closes-bug: #1931875 Change-Id: I0411d4aa6abd86cb38bf3c1999f2bae213983078 --- oslo_limit/limit.py | 68 ++++- oslo_limit/opts.py | 23 +- oslo_limit/tests/test_limit.py | 251 +++++++++++++++++- ...int-id-from-keystone-9d8419673902c258.yaml | 11 + 4 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/look-up-endpoint-id-from-keystone-9d8419673902c258.yaml diff --git a/oslo_limit/limit.py b/oslo_limit/limit.py index ef53fbc..bd8b1e6 100644 --- a/oslo_limit/limit.py +++ b/oslo_limit/limit.py @@ -18,6 +18,7 @@ from collections import namedtuple from keystoneauth1 import exceptions as ksa_exceptions from keystoneauth1 import loading from openstack import connection +from openstack import exceptions as os_exceptions from oslo_config import cfg from oslo_log import log @@ -262,16 +263,69 @@ class _EnforcerUtils(object): # {resource_name: registered_limit} self.rlimit_cache = {} - # get and cache endpoint info - endpoint_id = CONF.oslo_limit.endpoint_id - if not endpoint_id: - raise ValueError("endpoint_id is not configured") - self._endpoint = self.connection.get_endpoint(endpoint_id) - if not self._endpoint: - raise ValueError("can't find endpoint for %s" % endpoint_id) + self._endpoint = self._get_endpoint() self._service_id = self._endpoint.service_id self._region_id = self._endpoint.region_id + def _get_endpoint(self): + endpoint = self._get_endpoint_by_id() + if endpoint is not None: + return endpoint + + return self._get_endpoint_by_service_lookup() + + def _get_endpoint_by_id(self): + endpoint_id = CONF.oslo_limit.endpoint_id + if endpoint_id is None: + return None + + try: + endpoint = self.connection.get_endpoint(endpoint_id) + except os_exceptions.ResourceNotFound: + raise ValueError("Can't find endpoint for %s" % endpoint_id) + return endpoint + + def _get_endpoint_by_service_lookup(self): + service_type = CONF.oslo_limit.endpoint_service_type + service_name = CONF.oslo_limit.endpoint_service_name + if not service_type and not service_name: + raise ValueError( + "Either service_type or service_name should be set") + + try: + services = self.connection.services(type=service_type, + name=service_name) + if len(services) > 1: + raise ValueError("Multiple services found") + service_id = services[0].id + except os_exceptions.ResourceNotFound: + raise ValueError("Service not found") + + if CONF.oslo_limit.endpoint_region_name is not None: + try: + regions = self.connection.regions( + name=CONF.oslo_limit.endpoint_region_name) + if len(regions) > 1: + raise ValueError("Multiple regions found") + region_id = regions[0].id + except os_exceptions.ResourceNotFound: + raise ValueError("Region not found") + else: + region_id = None + + try: + endpoints = self.connection.endpoints( + service_id=service_id, region_id=region_id, + interface=CONF.oslo_limit.endpoint_interface, + ) + except os_exceptions.ResourceNotFound: + raise ValueError("Endpoint not found") + + if len(endpoints) > 1: + raise ValueError("Multiple endpoints found") + + return endpoints[0] + @staticmethod def enforce_limits(project_id, limits, current_usage, deltas): """Check that proposed usage is not over given limits diff --git a/oslo_limit/opts.py b/oslo_limit/opts.py index c23e627..9dd034a 100644 --- a/oslo_limit/opts.py +++ b/oslo_limit/opts.py @@ -24,12 +24,25 @@ __all__ = [ CONF = cfg.CONF -endpoint_id = cfg.StrOpt( - 'endpoint_id', - help=_("The service's endpoint id which is registered in Keystone.")) - _options = [ - endpoint_id, + cfg.StrOpt( + 'endpoint_id', + help=_("The service's endpoint id which is registered in Keystone.")), + cfg.StrOpt( + 'endpoint_service_name', + help=_("Service name for endpoint discovery")), + cfg.StrOpt( + 'endpoint_service_type', + help=_("Service type for endpoint discovery")), + cfg.StrOpt( + 'endpoint_region_name', + help=_("Region to which the endpoint belongs")), + cfg.StrOpt( + 'endpoint_interface', + default='publicURL', + choices=['public', 'publicURL', 'internal', 'internalURL', + 'admin', 'adminURL'], + help=_("The interface for endpoint discovery")), ] _option_group = 'oslo_limit' diff --git a/oslo_limit/tests/test_limit.py b/oslo_limit/tests/test_limit.py index 7a300bb..79e6e0a 100644 --- a/oslo_limit/tests/test_limit.py +++ b/oslo_limit/tests/test_limit.py @@ -21,9 +21,12 @@ Tests for `limit` module. from unittest import mock import uuid +from openstack import exceptions as os_exceptions from openstack.identity.v3 import endpoint from openstack.identity.v3 import limit as klimit +from openstack.identity.v3 import region from openstack.identity.v3 import registered_limit +from openstack.identity.v3 import service from oslo_config import cfg from oslo_config import fixture as config_fixture from oslotest import base @@ -337,6 +340,244 @@ class TestEnforcerUtils(base.BaseTestCase): self.assertEqual(fake_endpoint, utils._endpoint) self.mock_conn.get_endpoint.assert_called_once_with('ENDPOINT_ID') + self.mock_conn.services.assert_not_called() + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_no_id(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.mock_conn.get_endpoint.side_effect = \ + os_exceptions.ResourceNotFound + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_not_called() + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_missing(self): + self.mock_conn.get_endpoint.side_effect = \ + os_exceptions.ResourceNotFound + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + self.mock_conn.get_endpoint.assert_called_once_with('ENDPOINT_ID') + self.mock_conn.services.assert_not_called() + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_lookup_without_service_opts(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_not_called() + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_lookup(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + fake_endpoint = endpoint.Endpoint() + self.mock_conn.endpoints.return_value = [fake_endpoint] + + utils = limit._EnforcerUtils() + + self.assertEqual(fake_endpoint, utils._endpoint) + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.endpoints.assert_called_once_with( + service_id='SERVICE_ID', region_id=None, interface='publicURL') + + def test_get_endpoint_lookup_multiple_endpoints(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + self.mock_conn.endpoints.return_value = [ + endpoint.Endpoint(), endpoint.Endpoint() + ] + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.endpoints.assert_called_once_with( + service_id='SERVICE_ID', region_id=None, interface='publicURL') + + def test_get_endpoint_lookup_endpoint_not_found(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + self.mock_conn.endpoints.side_effect = os_exceptions.ResourceNotFound + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.endpoints.assert_called_once_with( + service_id='SERVICE_ID', region_id=None, interface='publicURL') + + def test_get_endpoint_lookup_multiple_service(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + self.mock_conn.services.side_effect = [ + service.Service(id='SERVICE_ID1'), + service.Service(id='SERVICE_ID2') + ] + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_lookup_service_not_found(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + self.mock_conn.services.side_effect = os_exceptions.ResourceNotFound + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_lookup_with_region(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_region_name='regionOne' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + fake_endpoint = endpoint.Endpoint() + self.mock_conn.endpoints.return_value = [fake_endpoint] + fake_region = region.Region(id='REGION_ID') + self.mock_conn.regions.return_value = [fake_region] + + utils = limit._EnforcerUtils() + + self.assertEqual(fake_endpoint, utils._endpoint) + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.regions.assert_called_once_with(name='regionOne') + self.mock_conn.endpoints.assert_called_once_with( + service_id='SERVICE_ID', region_id='REGION_ID', + interface='publicURL') + + def test_get_endpoint_lookup_with_region_not_found(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_region_name='regionOne' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + fake_endpoint = endpoint.Endpoint() + self.mock_conn.endpoints.return_value = [fake_endpoint] + self.mock_conn.regions.side_effect = os_exceptions.ResourceNotFound + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.regions.assert_called_once_with(name='regionOne') + self.mock_conn.endpoints.assert_not_called() + + def test_get_endpoint_lookup_with_mutliple_regions(self): + self.config_fixture.config(group='oslo_limit', endpoint_id=None) + self.config_fixture.config( + group='oslo_limit', endpoint_service_type='SERVICE_TYPE' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_service_name='SERVICE_NAME' + ) + self.config_fixture.config( + group='oslo_limit', endpoint_region_name='regionOne' + ) + fake_service = service.Service(id='SERVICE_ID') + self.mock_conn.services.return_value = [fake_service] + fake_endpoint = endpoint.Endpoint() + self.mock_conn.endpoints.return_value = [fake_endpoint] + self.mock_conn.regions.return_value = [ + region.Region(id='REGION_ID1'), region.Region(id='REGION_ID2')] + + self.assertRaises( + ValueError, + limit._EnforcerUtils + ) + + self.mock_conn.get_endpoint.assert_not_called() + self.mock_conn.services.assert_called_once_with(type='SERVICE_TYPE', + name='SERVICE_NAME') + self.mock_conn.regions.assert_called_once_with(name='regionOne') + self.mock_conn.endpoints.assert_not_called() def test_get_registered_limit_empty(self): self.mock_conn.registered_limits.return_value = iter([]) @@ -357,9 +598,8 @@ class TestEnforcerUtils(base.BaseTestCase): self.assertEqual(foo, reg_limit) def test_get_registered_limits(self): - fake_endpoint = endpoint.Endpoint() - fake_endpoint.service_id = "service_id" - fake_endpoint.region_id = "region_id" + fake_endpoint = endpoint.Endpoint(service_id='service_id', + region_id='region_id') self.mock_conn.get_endpoint.return_value = fake_endpoint # a and c have limits, b doesn't have one @@ -384,9 +624,8 @@ class TestEnforcerUtils(base.BaseTestCase): self.assertEqual([('a', 1), ('b', 0), ('c', 2)], limits) def test_get_project_limits(self): - fake_endpoint = endpoint.Endpoint() - fake_endpoint.service_id = "service_id" - fake_endpoint.region_id = "region_id" + fake_endpoint = endpoint.Endpoint(service_id='service_id', + region_id='region_id') self.mock_conn.get_endpoint.return_value = fake_endpoint project_id = uuid.uuid4().hex diff --git a/releasenotes/notes/look-up-endpoint-id-from-keystone-9d8419673902c258.yaml b/releasenotes/notes/look-up-endpoint-id-from-keystone-9d8419673902c258.yaml new file mode 100644 index 0000000..c892b62 --- /dev/null +++ b/releasenotes/notes/look-up-endpoint-id-from-keystone-9d8419673902c258.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The following options have been added to the ``[oslo_limit]`` section. + When these options are set instead of the ``endpoint_id`` option, endpoint + id is looked up from keystone API. + + - ``endpoint_service_name`` + - ``endpoint_service_type`` + - ``endpoint_region_name`` + - ``endpoint_interface``