Add filter for resource tag
Introduce a generic mechanism to allow the user to get Neutron resources with tag filter. APIImpact Change-Id: I1e5b25fea268e188cc9620c72b3c71004b197512 Partial-Implements: blueprint add-tags-to-core-resources Related-Bug: #1489291
This commit is contained in:
parent
ec1457dd75
commit
7fb8d542ef
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
from neutron.db import model_base
|
from neutron.db import model_base
|
||||||
|
|
||||||
@ -27,3 +28,75 @@ class Tag(model_base.BASEV2):
|
|||||||
standard_attr = orm.relationship(
|
standard_attr = orm.relationship(
|
||||||
'StandardAttribute',
|
'StandardAttribute',
|
||||||
backref=orm.backref('tags', lazy='joined', viewonly=True))
|
backref=orm.backref('tags', lazy='joined', viewonly=True))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tag_list(tag_strings):
|
||||||
|
tags = set()
|
||||||
|
for tag_str in tag_strings:
|
||||||
|
tags |= set(tag_str.split(','))
|
||||||
|
return list(tags)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_tag_filters(model, query, filters):
|
||||||
|
"""Apply tag filters
|
||||||
|
|
||||||
|
There are four types of filter:
|
||||||
|
`tags` -- One or more strings that will be used to filter results
|
||||||
|
in an AND expression: T1 AND T2
|
||||||
|
|
||||||
|
`tags-any` -- One or more strings that will be used to filter results
|
||||||
|
in an OR expression: T1 OR T2
|
||||||
|
|
||||||
|
`not-tags` -- One or more strings that will be used to filter results
|
||||||
|
in a NOT AND expression: NOT (T1 AND T2)
|
||||||
|
|
||||||
|
`not-tags-any` -- One or more strings that will be used to filter results
|
||||||
|
in a NOT OR expression: NOT (T1 OR T2)
|
||||||
|
|
||||||
|
Note: tag values can be specified comma separated string.
|
||||||
|
for example,
|
||||||
|
'GET /v2.0/networks?tags-any=red,blue' is equivalent to
|
||||||
|
'GET /v2.0/networks?tags-any=red&tags-any=blue'
|
||||||
|
it means 'red' or 'blue'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'tags' in filters:
|
||||||
|
tags = _get_tag_list(filters.pop('tags'))
|
||||||
|
first_tag = tags.pop(0)
|
||||||
|
query = query.join(Tag,
|
||||||
|
model.standard_attr_id == Tag.standard_attr_id)
|
||||||
|
query = query.filter(Tag.tag == first_tag)
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
tag_alias = aliased(Tag)
|
||||||
|
query = query.join(tag_alias,
|
||||||
|
model.standard_attr_id == tag_alias.standard_attr_id)
|
||||||
|
query = query.filter(tag_alias.tag == tag)
|
||||||
|
|
||||||
|
if 'tags-any' in filters:
|
||||||
|
tags = _get_tag_list(filters.pop('tags-any'))
|
||||||
|
query = query.join(Tag,
|
||||||
|
model.standard_attr_id == Tag.standard_attr_id)
|
||||||
|
query = query.filter(Tag.tag.in_(tags))
|
||||||
|
|
||||||
|
if 'not-tags' in filters:
|
||||||
|
tags = _get_tag_list(filters.pop('not-tags'))
|
||||||
|
first_tag = tags.pop(0)
|
||||||
|
subq = query.session.query(Tag.standard_attr_id)
|
||||||
|
subq = subq.filter(Tag.tag == first_tag)
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
tag_alias = aliased(Tag)
|
||||||
|
subq = subq.join(tag_alias,
|
||||||
|
Tag.standard_attr_id == tag_alias.standard_attr_id)
|
||||||
|
subq = subq.filter(tag_alias.tag == tag)
|
||||||
|
|
||||||
|
query = query.filter(~model.standard_attr_id.in_(subq))
|
||||||
|
|
||||||
|
if 'not-tags-any' in filters:
|
||||||
|
tags = _get_tag_list(filters.pop('not-tags-any'))
|
||||||
|
subq = query.session.query(Tag.standard_attr_id)
|
||||||
|
subq = subq.filter(Tag.tag.in_(tags))
|
||||||
|
query = query.filter(~model.standard_attr_id.in_(subq))
|
||||||
|
|
||||||
|
return query
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import functools
|
||||||
|
|
||||||
from oslo_db import api as oslo_db_api
|
from oslo_db import api as oslo_db_api
|
||||||
from oslo_db import exception as db_exc
|
from oslo_db import exception as db_exc
|
||||||
from oslo_log import helpers as log_helpers
|
from oslo_log import helpers as log_helpers
|
||||||
@ -118,6 +120,9 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase):
|
|||||||
|
|
||||||
# support only _apply_dict_extend_functions supported resources
|
# support only _apply_dict_extend_functions supported resources
|
||||||
# at the moment.
|
# at the moment.
|
||||||
for resource in resource_model_map:
|
for resource, model in resource_model_map.items():
|
||||||
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
|
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
|
||||||
resource, [_extend_tags_dict])
|
resource, [_extend_tags_dict])
|
||||||
|
common_db_mixin.CommonDbMixin.register_model_query_hook(
|
||||||
|
model, "tag", None, None,
|
||||||
|
functools.partial(tag_model.apply_tag_filters, model))
|
||||||
|
@ -65,6 +65,28 @@ class TestTagApiBase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
|
|||||||
def _assertEqualTags(self, expected, actual):
|
def _assertEqualTags(self, expected, actual):
|
||||||
self.assertEqual(set(expected), set(actual))
|
self.assertEqual(set(expected), set(actual))
|
||||||
|
|
||||||
|
def _make_query_string(self, tags, tags_any, not_tags, not_tags_any):
|
||||||
|
filter_strings = []
|
||||||
|
if tags:
|
||||||
|
filter_strings.append("tags=" + ','.join(tags))
|
||||||
|
if tags_any:
|
||||||
|
filter_strings.append("tags-any=" + ','.join(tags_any))
|
||||||
|
if not_tags:
|
||||||
|
filter_strings.append("not-tags=" + ','.join(not_tags))
|
||||||
|
if not_tags_any:
|
||||||
|
filter_strings.append("not-tags-any=" + ','.join(not_tags_any))
|
||||||
|
|
||||||
|
return '&'.join(filter_strings)
|
||||||
|
|
||||||
|
def _get_tags_filter_resources(self, tags=None, tags_any=None,
|
||||||
|
not_tags=None, not_tags_any=None):
|
||||||
|
params = self._make_query_string(tags, tags_any, not_tags,
|
||||||
|
not_tags_any)
|
||||||
|
req = self._req('GET', self.resource, params=params)
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
res = self.deserialize(self.fmt, res)
|
||||||
|
return res[self.resource]
|
||||||
|
|
||||||
|
|
||||||
class TestNetworkTagApi(TestTagApiBase):
|
class TestNetworkTagApi(TestTagApiBase):
|
||||||
resource = 'networks'
|
resource = 'networks'
|
||||||
@ -153,3 +175,68 @@ class TestNetworkTagApi(TestTagApiBase):
|
|||||||
self.assertEqual(204, res.status_int)
|
self.assertEqual(204, res.status_int)
|
||||||
tags = self._get_resource_tags(net_id)
|
tags = self._get_resource_tags(net_id)
|
||||||
self._assertEqualTags([], tags)
|
self._assertEqualTags([], tags)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetworkTagFilter(TestTagApiBase):
|
||||||
|
resource = 'networks'
|
||||||
|
member = 'network'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNetworkTagFilter, self).setUp()
|
||||||
|
self._prepare_network_tags()
|
||||||
|
|
||||||
|
def _prepare_network_tags(self):
|
||||||
|
res = self._make_network(self.fmt, 'net1', True)
|
||||||
|
net1_id = res['network']['id']
|
||||||
|
res = self._make_network(self.fmt, 'net2', True)
|
||||||
|
net2_id = res['network']['id']
|
||||||
|
res = self._make_network(self.fmt, 'net3', True)
|
||||||
|
net3_id = res['network']['id']
|
||||||
|
res = self._make_network(self.fmt, 'net4', True)
|
||||||
|
net4_id = res['network']['id']
|
||||||
|
res = self._make_network(self.fmt, 'net5', True)
|
||||||
|
net5_id = res['network']['id']
|
||||||
|
|
||||||
|
self._put_tags(net1_id, ['red'])
|
||||||
|
self._put_tags(net2_id, ['red', 'blue'])
|
||||||
|
self._put_tags(net3_id, ['red', 'blue', 'green'])
|
||||||
|
self._put_tags(net4_id, ['green'])
|
||||||
|
# net5: no tags
|
||||||
|
tags = self._get_resource_tags(net5_id)
|
||||||
|
self._assertEqualTags([], tags)
|
||||||
|
|
||||||
|
def _assertEqualResources(self, expected, res):
|
||||||
|
actual = [n['name'] for n in res]
|
||||||
|
self.assertEqual(set(expected), set(actual))
|
||||||
|
|
||||||
|
def test_filter_tags_single(self):
|
||||||
|
res = self._get_tags_filter_resources(tags=['red'])
|
||||||
|
self._assertEqualResources(['net1', 'net2', 'net3'], res)
|
||||||
|
|
||||||
|
def test_filter_tags_multi(self):
|
||||||
|
res = self._get_tags_filter_resources(tags=['red', 'blue'])
|
||||||
|
self._assertEqualResources(['net2', 'net3'], res)
|
||||||
|
|
||||||
|
def test_filter_tags_any_single(self):
|
||||||
|
res = self._get_tags_filter_resources(tags_any=['blue'])
|
||||||
|
self._assertEqualResources(['net2', 'net3'], res)
|
||||||
|
|
||||||
|
def test_filter_tags_any_multi(self):
|
||||||
|
res = self._get_tags_filter_resources(tags_any=['red', 'blue'])
|
||||||
|
self._assertEqualResources(['net1', 'net2', 'net3'], res)
|
||||||
|
|
||||||
|
def test_filter_not_tags_single(self):
|
||||||
|
res = self._get_tags_filter_resources(not_tags=['red'])
|
||||||
|
self._assertEqualResources(['net4', 'net5'], res)
|
||||||
|
|
||||||
|
def test_filter_not_tags_multi(self):
|
||||||
|
res = self._get_tags_filter_resources(not_tags=['red', 'blue'])
|
||||||
|
self._assertEqualResources(['net1', 'net4', 'net5'], res)
|
||||||
|
|
||||||
|
def test_filter_not_tags_any_single(self):
|
||||||
|
res = self._get_tags_filter_resources(not_tags_any=['blue'])
|
||||||
|
self._assertEqualResources(['net1', 'net4', 'net5'], res)
|
||||||
|
|
||||||
|
def test_filter_not_tags_any_multi(self):
|
||||||
|
res = self._get_tags_filter_resources(not_tags_any=['red', 'blue'])
|
||||||
|
self._assertEqualResources(['net4', 'net5'], res)
|
||||||
|
@ -3,3 +3,5 @@ prelude: >
|
|||||||
Add tag mechanism for network resources
|
Add tag mechanism for network resources
|
||||||
features:
|
features:
|
||||||
- Users can set tags on their network resources.
|
- Users can set tags on their network resources.
|
||||||
|
- Networks can be filtered by tags. The supported filters are
|
||||||
|
'tags', 'tags-any', 'not-tags' and 'not-tags-any'.
|
||||||
|
Loading…
Reference in New Issue
Block a user