Merge "Deprecate `endpoint_filter.sql` backend"
This commit is contained in:
commit
474b762561
|
@ -19,6 +19,7 @@ import sqlalchemy
|
||||||
from sqlalchemy.sql import true
|
from sqlalchemy.sql import true
|
||||||
|
|
||||||
from keystone.catalog.backends import base
|
from keystone.catalog.backends import base
|
||||||
|
from keystone.common import dependency
|
||||||
from keystone.common import driver_hints
|
from keystone.common import driver_hints
|
||||||
from keystone.common import sql
|
from keystone.common import sql
|
||||||
from keystone.common import utils
|
from keystone.common import utils
|
||||||
|
@ -81,6 +82,7 @@ class Endpoint(sql.ModelBase, sql.DictBase):
|
||||||
extra = sql.Column(sql.JsonBlob())
|
extra = sql.Column(sql.JsonBlob())
|
||||||
|
|
||||||
|
|
||||||
|
@dependency.requires('catalog_api')
|
||||||
class Catalog(base.CatalogDriverBase):
|
class Catalog(base.CatalogDriverBase):
|
||||||
# Regions
|
# Regions
|
||||||
def list_regions(self, hints):
|
def list_regions(self, hints):
|
||||||
|
@ -373,7 +375,52 @@ class Catalog(base.CatalogDriverBase):
|
||||||
service['name'] = svc.extra.get('name', '')
|
service['name'] = svc.extra.get('name', '')
|
||||||
return service
|
return service
|
||||||
|
|
||||||
return [make_v3_service(svc) for svc in services]
|
# Build the unfiltered catalog, this is the catalog that is
|
||||||
|
# returned if endpoint filtering is not performed and the
|
||||||
|
# option of `return_all_endpoints_if_no_filter` is set to true.
|
||||||
|
catalog_ref = [make_v3_service(svc) for svc in services]
|
||||||
|
|
||||||
|
# Filter the `catalog_ref` above by any project-endpoint
|
||||||
|
# association configured by endpoint filter.
|
||||||
|
filtered_endpoints = {}
|
||||||
|
if tenant_id:
|
||||||
|
filtered_endpoints = (
|
||||||
|
self.catalog_api.list_endpoints_for_project(tenant_id))
|
||||||
|
# endpoint filter is enabled, only return the filtered endpoints.
|
||||||
|
if filtered_endpoints:
|
||||||
|
filtered_ids = list(filtered_endpoints.keys())
|
||||||
|
# This is actually working on the copy of `catalog_ref` since
|
||||||
|
# the index will be shifted if remove/add any entry for the
|
||||||
|
# original one.
|
||||||
|
for service in catalog_ref[:]:
|
||||||
|
endpoints = service['endpoints']
|
||||||
|
for endpoint in endpoints[:]:
|
||||||
|
endpoint_id = endpoint['id']
|
||||||
|
# remove the endpoint that is not associated with
|
||||||
|
# the project.
|
||||||
|
if endpoint_id not in filtered_ids:
|
||||||
|
service['endpoints'].remove(endpoint)
|
||||||
|
continue
|
||||||
|
# remove the disabled endpoint from the list.
|
||||||
|
if not filtered_endpoints[endpoint_id]['enabled']:
|
||||||
|
service['endpoints'].remove(endpoint)
|
||||||
|
# NOTE(davechen): The service will not be included in the
|
||||||
|
# catalog if the service doesn't have any endpoint when
|
||||||
|
# endpoint filter is enabled, this is inconsistent with
|
||||||
|
# full catalog that is returned when endpoint filter is
|
||||||
|
# disabled.
|
||||||
|
if not service.get('endpoints'):
|
||||||
|
catalog_ref.remove(service)
|
||||||
|
# When it arrives here it means it's domain scoped token (
|
||||||
|
# `tenant_id` is not set) or it's a project scoped token
|
||||||
|
# but the endpoint filtering is not performed.
|
||||||
|
# Both of them tell us the endpoint filtering is not enabled, so
|
||||||
|
# check the option of `return_all_endpoints_if_no_filter`, it will
|
||||||
|
# judge whether a full unfiltered catalog or a empty service
|
||||||
|
# catalog will be returned.
|
||||||
|
elif not CONF.endpoint_filter.return_all_endpoints_if_no_filter:
|
||||||
|
return []
|
||||||
|
return catalog_ref
|
||||||
|
|
||||||
@sql.handle_conflicts(conflict_type='project_endpoint')
|
@sql.handle_conflicts(conflict_type='project_endpoint')
|
||||||
def add_endpoint_to_project(self, endpoint_id, project_id):
|
def add_endpoint_to_project(self, endpoint_id, project_id):
|
||||||
|
|
|
@ -12,66 +12,22 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_log import versionutils
|
||||||
|
|
||||||
from keystone.catalog.backends import sql
|
from keystone.catalog.backends import sql
|
||||||
from keystone.common import dependency
|
|
||||||
from keystone.common import utils
|
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
|
|
||||||
|
|
||||||
CONF = keystone.conf.CONF
|
CONF = keystone.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
@dependency.requires('catalog_api')
|
@versionutils.deprecated(
|
||||||
|
what=('keystone.contrib.endpoint_filter.'
|
||||||
|
'backends.catalog_sql.EndPointFilterCatalog'),
|
||||||
|
as_of=versionutils.deprecated.OCATA,
|
||||||
|
remove_in=+1,
|
||||||
|
in_favor_of='keystone.catalog.backends.sql.Catalog')
|
||||||
class EndpointFilterCatalog(sql.Catalog):
|
class EndpointFilterCatalog(sql.Catalog):
|
||||||
def get_v3_catalog(self, user_id, project_id):
|
def get_v3_catalog(self, user_id, project_id):
|
||||||
substitutions = dict(CONF.items())
|
|
||||||
substitutions.update({
|
|
||||||
'tenant_id': project_id,
|
|
||||||
'project_id': project_id,
|
|
||||||
'user_id': user_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
services = {}
|
|
||||||
|
|
||||||
dict_of_endpoint_refs = (self.catalog_api.
|
|
||||||
list_endpoints_for_project(project_id))
|
|
||||||
|
|
||||||
if (not dict_of_endpoint_refs and
|
|
||||||
CONF.endpoint_filter.return_all_endpoints_if_no_filter):
|
|
||||||
return super(EndpointFilterCatalog, self).get_v3_catalog(
|
return super(EndpointFilterCatalog, self).get_v3_catalog(
|
||||||
user_id, project_id)
|
user_id, project_id)
|
||||||
|
|
||||||
for endpoint_id, endpoint in dict_of_endpoint_refs.items():
|
|
||||||
if not endpoint['enabled']:
|
|
||||||
# Skip disabled endpoints.
|
|
||||||
continue
|
|
||||||
service_id = endpoint['service_id']
|
|
||||||
services.setdefault(
|
|
||||||
service_id,
|
|
||||||
self.get_service(service_id))
|
|
||||||
service = services[service_id]
|
|
||||||
del endpoint['service_id']
|
|
||||||
del endpoint['enabled']
|
|
||||||
del endpoint['legacy_endpoint_id']
|
|
||||||
# Include deprecated region for backwards compatibility
|
|
||||||
endpoint['region'] = endpoint['region_id']
|
|
||||||
endpoint['url'] = utils.format_url(
|
|
||||||
endpoint['url'], substitutions)
|
|
||||||
# populate filtered endpoints
|
|
||||||
if 'endpoints' in services[service_id]:
|
|
||||||
service['endpoints'].append(endpoint)
|
|
||||||
else:
|
|
||||||
service['endpoints'] = [endpoint]
|
|
||||||
|
|
||||||
# format catalog
|
|
||||||
catalog = []
|
|
||||||
for service_id, service in services.items():
|
|
||||||
formatted_service = {}
|
|
||||||
formatted_service['id'] = service['id']
|
|
||||||
formatted_service['type'] = service['type']
|
|
||||||
formatted_service['name'] = service['name']
|
|
||||||
formatted_service['endpoints'] = service['endpoints']
|
|
||||||
catalog.append(formatted_service)
|
|
||||||
|
|
||||||
return catalog
|
|
||||||
|
|
|
@ -560,8 +560,11 @@ class CatalogTests(object):
|
||||||
enabled_endpoint_ref = self._create_endpoints()[1]
|
enabled_endpoint_ref = self._create_endpoints()[1]
|
||||||
|
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
project_id = uuid.uuid4().hex
|
# Use the project created by the default fixture since the project
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, project_id)
|
# should exist if we want to filter the catalog by the project or
|
||||||
|
# replace the url with a valid project id.
|
||||||
|
catalog = self.catalog_api.get_v3_catalog(user_id,
|
||||||
|
self.tenant_bar['id'])
|
||||||
|
|
||||||
endpoint_ids = [x['id'] for x in catalog[0]['endpoints']]
|
endpoint_ids = [x['id'] for x in catalog[0]['endpoints']]
|
||||||
self.assertEqual([enabled_endpoint_ref['id']], endpoint_ids)
|
self.assertEqual([enabled_endpoint_ref['id']], endpoint_ids)
|
||||||
|
|
|
@ -827,6 +827,70 @@ class SqlCatalog(SqlTests, catalog_tests.CatalogTests):
|
||||||
self.catalog_api.delete_region,
|
self.catalog_api.delete_region,
|
||||||
region['id'])
|
region['id'])
|
||||||
|
|
||||||
|
def test_v3_catalog_domain_scoped_token(self):
|
||||||
|
# test the case that tenant_id is None.
|
||||||
|
srv_1 = unit.new_service_ref()
|
||||||
|
self.catalog_api.create_service(srv_1['id'], srv_1)
|
||||||
|
endpoint_1 = unit.new_endpoint_ref(service_id=srv_1['id'],
|
||||||
|
region_id=None)
|
||||||
|
self.catalog_api.create_endpoint(endpoint_1['id'], endpoint_1)
|
||||||
|
|
||||||
|
srv_2 = unit.new_service_ref()
|
||||||
|
self.catalog_api.create_service(srv_2['id'], srv_2)
|
||||||
|
endpoint_2 = unit.new_endpoint_ref(service_id=srv_2['id'],
|
||||||
|
region_id=None)
|
||||||
|
self.catalog_api.create_endpoint(endpoint_2['id'], endpoint_2)
|
||||||
|
|
||||||
|
self.config_fixture.config(group='endpoint_filter',
|
||||||
|
return_all_endpoints_if_no_filter=True)
|
||||||
|
catalog_ref = self.catalog_api.get_v3_catalog(uuid.uuid4().hex, None)
|
||||||
|
self.assertThat(catalog_ref, matchers.HasLength(2))
|
||||||
|
self.config_fixture.config(group='endpoint_filter',
|
||||||
|
return_all_endpoints_if_no_filter=False)
|
||||||
|
catalog_ref = self.catalog_api.get_v3_catalog(uuid.uuid4().hex, None)
|
||||||
|
self.assertThat(catalog_ref, matchers.HasLength(0))
|
||||||
|
|
||||||
|
def test_v3_catalog_endpoint_filter_enabled(self):
|
||||||
|
srv_1 = unit.new_service_ref()
|
||||||
|
self.catalog_api.create_service(srv_1['id'], srv_1)
|
||||||
|
endpoint_1 = unit.new_endpoint_ref(service_id=srv_1['id'],
|
||||||
|
region_id=None)
|
||||||
|
self.catalog_api.create_endpoint(endpoint_1['id'], endpoint_1)
|
||||||
|
endpoint_2 = unit.new_endpoint_ref(service_id=srv_1['id'],
|
||||||
|
region_id=None)
|
||||||
|
self.catalog_api.create_endpoint(endpoint_2['id'], endpoint_2)
|
||||||
|
# create endpoint-project association.
|
||||||
|
self.catalog_api.add_endpoint_to_project(
|
||||||
|
endpoint_1['id'],
|
||||||
|
self.tenant_bar['id'])
|
||||||
|
|
||||||
|
catalog_ref = self.catalog_api.get_v3_catalog(uuid.uuid4().hex,
|
||||||
|
self.tenant_bar['id'])
|
||||||
|
self.assertThat(catalog_ref, matchers.HasLength(1))
|
||||||
|
self.assertThat(catalog_ref[0]['endpoints'], matchers.HasLength(1))
|
||||||
|
# the endpoint is that defined in the endpoint-project association.
|
||||||
|
self.assertEqual(endpoint_1['id'],
|
||||||
|
catalog_ref[0]['endpoints'][0]['id'])
|
||||||
|
|
||||||
|
def test_v3_catalog_endpoint_filter_disabled(self):
|
||||||
|
# there is no endpoint-project association defined.
|
||||||
|
self.config_fixture.config(group='endpoint_filter',
|
||||||
|
return_all_endpoints_if_no_filter=True)
|
||||||
|
srv_1 = unit.new_service_ref()
|
||||||
|
self.catalog_api.create_service(srv_1['id'], srv_1)
|
||||||
|
endpoint_1 = unit.new_endpoint_ref(service_id=srv_1['id'],
|
||||||
|
region_id=None)
|
||||||
|
self.catalog_api.create_endpoint(endpoint_1['id'], endpoint_1)
|
||||||
|
|
||||||
|
srv_2 = unit.new_service_ref()
|
||||||
|
self.catalog_api.create_service(srv_2['id'], srv_2)
|
||||||
|
|
||||||
|
catalog_ref = self.catalog_api.get_v3_catalog(uuid.uuid4().hex,
|
||||||
|
self.tenant_bar['id'])
|
||||||
|
self.assertThat(catalog_ref, matchers.HasLength(2))
|
||||||
|
srv_id_list = [catalog_ref[0]['id'], catalog_ref[1]['id']]
|
||||||
|
self.assertItemsEqual([srv_1['id'], srv_2['id']], srv_id_list)
|
||||||
|
|
||||||
|
|
||||||
class SqlPolicy(SqlTests, policy_tests.PolicyTests):
|
class SqlPolicy(SqlTests, policy_tests.PolicyTests):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -18,7 +18,6 @@ import uuid
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
|
|
||||||
from keystone import catalog
|
|
||||||
from keystone.tests import unit
|
from keystone.tests import unit
|
||||||
from keystone.tests.unit.ksfixtures import database
|
from keystone.tests.unit.ksfixtures import database
|
||||||
from keystone.tests.unit import test_v3
|
from keystone.tests.unit import test_v3
|
||||||
|
@ -803,7 +802,7 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCatalogAPISQL, self).setUp()
|
super(TestCatalogAPISQL, self).setUp()
|
||||||
self.useFixture(database.Database())
|
self.useFixture(database.Database())
|
||||||
self.catalog_api = catalog.Manager()
|
self.load_backends()
|
||||||
|
|
||||||
service = unit.new_service_ref()
|
service = unit.new_service_ref()
|
||||||
self.service_id = service['id']
|
self.service_id = service['id']
|
||||||
|
@ -824,10 +823,17 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
|
|
||||||
def test_get_catalog_ignores_endpoints_with_invalid_urls(self):
|
def test_get_catalog_ignores_endpoints_with_invalid_urls(self):
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
tenant_id = uuid.uuid4().hex
|
|
||||||
|
# create a project since the project should exist if we want to
|
||||||
|
# filter the catalog by the project or replace the url with a
|
||||||
|
# valid project id.
|
||||||
|
domain = unit.new_domain_ref()
|
||||||
|
self.resource_api.create_domain(domain['id'], domain)
|
||||||
|
project = unit.new_project_ref(domain_id=domain['id'])
|
||||||
|
self.resource_api.create_project(project['id'], project)
|
||||||
|
|
||||||
# the only endpoint in the catalog is the one created in setUp
|
# the only endpoint in the catalog is the one created in setUp
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
self.assertEqual(1, len(catalog[0]['endpoints']))
|
self.assertEqual(1, len(catalog[0]['endpoints']))
|
||||||
# it's also the only endpoint in the backend
|
# it's also the only endpoint in the backend
|
||||||
self.assertEqual(1, len(self.catalog_api.list_endpoints()))
|
self.assertEqual(1, len(self.catalog_api.list_endpoints()))
|
||||||
|
@ -841,7 +847,7 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
url='http://keystone/%(you_wont_find_me)s')
|
url='http://keystone/%(you_wont_find_me)s')
|
||||||
|
|
||||||
# verify that the invalid endpoints don't appear in the catalog
|
# verify that the invalid endpoints don't appear in the catalog
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
self.assertEqual(1, len(catalog[0]['endpoints']))
|
self.assertEqual(1, len(catalog[0]['endpoints']))
|
||||||
# all three appear in the backend
|
# all three appear in the backend
|
||||||
self.assertEqual(3, len(self.catalog_api.list_endpoints()))
|
self.assertEqual(3, len(self.catalog_api.list_endpoints()))
|
||||||
|
@ -851,7 +857,7 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
url='http://keystone/%(tenant_id)s')
|
url='http://keystone/%(tenant_id)s')
|
||||||
|
|
||||||
# there are two valid endpoints, positive check
|
# there are two valid endpoints, positive check
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
self.assertThat(catalog[0]['endpoints'], matchers.HasLength(2))
|
self.assertThat(catalog[0]['endpoints'], matchers.HasLength(2))
|
||||||
|
|
||||||
# If the URL has no 'tenant_id' to substitute, we will skip the
|
# If the URL has no 'tenant_id' to substitute, we will skip the
|
||||||
|
@ -862,7 +868,13 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
|
|
||||||
def test_get_catalog_always_returns_service_name(self):
|
def test_get_catalog_always_returns_service_name(self):
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
tenant_id = uuid.uuid4().hex
|
# create a project since the project should exist if we want to
|
||||||
|
# filter the catalog by the project or replace the url with a
|
||||||
|
# valid project id.
|
||||||
|
domain = unit.new_domain_ref()
|
||||||
|
self.resource_api.create_domain(domain['id'], domain)
|
||||||
|
project = unit.new_project_ref(domain_id=domain['id'])
|
||||||
|
self.resource_api.create_project(project['id'], project)
|
||||||
|
|
||||||
# create a service, with a name
|
# create a service, with a name
|
||||||
named_svc = unit.new_service_ref()
|
named_svc = unit.new_service_ref()
|
||||||
|
@ -875,7 +887,7 @@ class TestCatalogAPISQL(unit.TestCase):
|
||||||
self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc)
|
self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc)
|
||||||
self.create_endpoint(service_id=unnamed_svc['id'])
|
self.create_endpoint(service_id=unnamed_svc['id'])
|
||||||
|
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
|
|
||||||
named_endpoint = [ep for ep in catalog
|
named_endpoint = [ep for ep in catalog
|
||||||
if ep['type'] == named_svc['type']][0]
|
if ep['type'] == named_svc['type']][0]
|
||||||
|
@ -894,7 +906,7 @@ class TestCatalogAPISQLRegions(unit.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCatalogAPISQLRegions, self).setUp()
|
super(TestCatalogAPISQLRegions, self).setUp()
|
||||||
self.useFixture(database.Database())
|
self.useFixture(database.Database())
|
||||||
self.catalog_api = catalog.Manager()
|
self.load_backends()
|
||||||
|
|
||||||
def config_overrides(self):
|
def config_overrides(self):
|
||||||
super(TestCatalogAPISQLRegions, self).config_overrides()
|
super(TestCatalogAPISQLRegions, self).config_overrides()
|
||||||
|
@ -910,10 +922,15 @@ class TestCatalogAPISQLRegions(unit.TestCase):
|
||||||
del endpoint['region_id']
|
del endpoint['region_id']
|
||||||
self.catalog_api.create_endpoint(endpoint['id'], endpoint)
|
self.catalog_api.create_endpoint(endpoint['id'], endpoint)
|
||||||
|
|
||||||
|
# create a project since the project should exist if we want to
|
||||||
|
# filter the catalog by the project or replace the url with a
|
||||||
|
# valid project id.
|
||||||
|
domain = unit.new_domain_ref()
|
||||||
|
self.resource_api.create_domain(domain['id'], domain)
|
||||||
|
project = unit.new_project_ref(domain_id=domain['id'])
|
||||||
|
self.resource_api.create_project(project['id'], project)
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
tenant_id = uuid.uuid4().hex
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
|
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
|
||||||
self.assertValidCatalogEndpoint(
|
self.assertValidCatalogEndpoint(
|
||||||
catalog[0]['endpoints'][0], ref=endpoint)
|
catalog[0]['endpoints'][0], ref=endpoint)
|
||||||
|
|
||||||
|
@ -929,9 +946,15 @@ class TestCatalogAPISQLRegions(unit.TestCase):
|
||||||
|
|
||||||
endpoint = self.catalog_api.get_endpoint(endpoint['id'])
|
endpoint = self.catalog_api.get_endpoint(endpoint['id'])
|
||||||
user_id = uuid.uuid4().hex
|
user_id = uuid.uuid4().hex
|
||||||
tenant_id = uuid.uuid4().hex
|
# create a project since the project should exist if we want to
|
||||||
|
# filter the catalog by the project or replace the url with a
|
||||||
|
# valid project id.
|
||||||
|
domain = unit.new_domain_ref()
|
||||||
|
self.resource_api.create_domain(domain['id'], domain)
|
||||||
|
project = unit.new_project_ref(domain_id=domain['id'])
|
||||||
|
self.resource_api.create_project(project['id'], project)
|
||||||
|
|
||||||
catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id)
|
catalog = self.catalog_api.get_v3_catalog(user_id, project['id'])
|
||||||
self.assertValidCatalogEndpoint(
|
self.assertValidCatalogEndpoint(
|
||||||
catalog[0]['endpoints'][0], ref=endpoint)
|
catalog[0]['endpoints'][0], ref=endpoint)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
deprecations:
|
||||||
|
- >
|
||||||
|
[`blueprint deprecated-as-of-ocata <https://blueprints.launchpad.net/keystone/+spec/deprecated-as-of-ocata>`_]
|
||||||
|
The catalog backend ``endpoint_filter.sql``has been deprecated in the
|
||||||
|
`Ocata` release, it has been consolidated with the ``sql`` backend.
|
||||||
|
It's recommended to replace the ``endpoint_filter.sql`` catalog backend
|
||||||
|
with the ``sql`` backend. The ``endpoint_filter.sql`` backend will be
|
||||||
|
removed in the `Pike` release.
|
Loading…
Reference in New Issue