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
This commit is contained in:
Takashi Kajinami 2024-03-30 22:25:14 +09:00
parent ba8b9aba0b
commit 9575a24796
4 changed files with 335 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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