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
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from neutron.db import model_base
|
||||
|
||||
@ -27,3 +28,75 @@ class Tag(model_base.BASEV2):
|
||||
standard_attr = orm.relationship(
|
||||
'StandardAttribute',
|
||||
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.
|
||||
#
|
||||
|
||||
import functools
|
||||
|
||||
from oslo_db import api as oslo_db_api
|
||||
from oslo_db import exception as db_exc
|
||||
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
|
||||
# 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(
|
||||
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):
|
||||
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):
|
||||
resource = 'networks'
|
||||
@ -153,3 +175,68 @@ class TestNetworkTagApi(TestTagApiBase):
|
||||
self.assertEqual(204, res.status_int)
|
||||
tags = self._get_resource_tags(net_id)
|
||||
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
|
||||
features:
|
||||
- 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