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:
Hirofumi Ichihara 2016-03-01 11:57:36 +09:00
parent ec1457dd75
commit 7fb8d542ef
4 changed files with 168 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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