remove tag and tag_ext extensions

The tag and tag_ext extensions are deprecated for removal, but are not
used widely today [1]. Rather than rehoming these extensions to
neutron-lib and carrying out their deprecation life-cycle for no
apparent reason, this patch proposes we just remove them now.

While [2] initially removed these extensions, we had to revert
them with [3].

Depends-On: I295a5b84eb7fa3439561fa009b7499f94d8df4d2

[1] http://codesearch.openstack.org/?q=from%20neutron.extensions%20import%20(tag_ext%7Ctag)
[2] I0d7bcd789b468b1dd3f7ea13e6751a46203d6778
[3] If16443616eee703b66d57b6422dd451a443fbc64

Change-Id: I97095453610fff114d999a526d67e78119546ff5
This commit is contained in:
Boden R 2018-01-17 15:50:26 -07:00
parent 3ff872b913
commit 87a36cacc6
9 changed files with 4 additions and 620 deletions

View File

@ -30,7 +30,6 @@ scalability and HA.
| agent | agent |
| Subnet Allocation | subnet_allocation |
| L3 Agent Scheduler | l3_agent_scheduler |
| Tag support | tag |
| Neutron external network | external-net |
| Neutron Service Flavors | flavors |
| Network MTU | net-mtu |
@ -45,7 +44,6 @@ scalability and HA.
| Resource timestamps | standard-attr-timestamp |
| Neutron Service Type Management | service-type |
| Router Flavor Extension | l3-flavors |
| Tag support for resources: subnet, subnetpool, port, router | tag-ext |
| Neutron Extra DHCP opts | extra_dhcp_opt |
| Resource revision numbers | standard-attr-revisions |
| Pagination support | pagination |

View File

@ -50,12 +50,7 @@ Which Resources
---------------
Tag system uses standardattr mechanism so it's targeting to resources that have
the mechanism. The system is provided by 'tag' extension, 'tag-ext'
extension, and 'tagging' extension. The 'tag' extension supports networks only.
The 'tag-ext' extension supports subnets, ports, routers, and subnet pools.
The 'tagging' extension supports resources with standard attribute so it
means that 'tag' and 'tag-ext' extensions are unnecessary now. These extensions
will be removed. Some resources with standard attribute don't suit fit tag
the mechanism. Some resources with standard attribute don't suit fit tag
support usecases (e.g. security_group_rule). If new tag support resource is
added, the resource model should inherit HasStandardAttributes and then it must
implement the property 'api_parent' and 'tag_support'. And also the change

View File

@ -40,8 +40,6 @@ Verify operation
| | | subnets from a subnet pool |
| DHCP Agent Scheduler | dhcp_agent_scheduler | Schedule networks among |
| | | dhcp agents |
| Tag support | tag | Enables to set tag on |
| | | resources. |
| Neutron external network | external-net | Adds external network |
| | | attribute to network |
| | | resource. |
@ -73,7 +71,6 @@ Verify operation
| Neutron Service Type | service-type | API for retrieving service |
| Management | | providers for Neutron |
| | | advanced services |
| Tag support for | tag-ext | Extends tag support to |
| resources: subnet, | | more L2 and L3 resources. |
| subnetpool, port, router | | |
| Neutron Extra DHCP opts | extra_dhcp_opt | Extra options |

View File

@ -1,85 +0,0 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import network
from neutron_lib.api import extensions as api_extensions
from neutron_lib.api import faults
from neutron_lib.plugins import directory
from neutron.api import extensions
from neutron.api.v2 import resource as api_resource
from neutron.extensions import tagging
# This extension is deprecated because tagging supports all resources
TAG_SUPPORTED_RESOURCES = {
# We shouldn't add new resources here. If more resources need to be tagged,
# we must add them in new extension.
network.COLLECTION_NAME: network.RESOURCE_NAME,
}
class TagController(tagging.TaggingController):
def __init__(self):
self.plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE)
self.supported_resources = TAG_SUPPORTED_RESOURCES
class Tag(api_extensions.ExtensionDescriptor):
"""Extension class supporting tags."""
@classmethod
def get_name(cls):
return "Tag support"
@classmethod
def get_alias(cls):
return "tag"
@classmethod
def get_description(cls):
return "Enables to set tag on resources."
@classmethod
def get_updated(cls):
return "2016-01-01T00:00:00-00:00"
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
exts = []
action_status = {'index': 200, 'show': 204, 'update': 201,
'update_all': 200, 'delete': 204, 'delete_all': 204}
controller = api_resource.Resource(TagController(),
faults.FAULT_MAP,
action_status=action_status)
collection_methods = {"delete_all": "DELETE",
"update_all": "PUT"}
exts = []
for collection_name, member_name in TAG_SUPPORTED_RESOURCES.items():
parent = {'member_name': member_name,
'collection_name': collection_name}
exts.append(extensions.ResourceExtension(
tagging.TAGS, controller, parent,
collection_methods=collection_methods))
return exts
def get_extended_resources(self, version):
if version != "2.0":
return {}
EXTENDED_ATTRIBUTES_2_0 = {}
for collection_name in TAG_SUPPORTED_RESOURCES:
EXTENDED_ATTRIBUTES_2_0[collection_name] = (
tagging.TAG_ATTRIBUTE_MAP)
return EXTENDED_ATTRIBUTES_2_0

View File

@ -1,95 +0,0 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.api.definitions import subnetpool as subnetpool_def
from neutron_lib.api import extensions as api_extensions
from neutron_lib.api import faults
from neutron_lib.plugins import directory
from neutron.api import extensions
from neutron.api.v2 import resource as api_resource
from neutron.extensions import tagging
# This extension is deprecated because tagging supports all resources
TAG_SUPPORTED_RESOURCES = {
# We shouldn't add new resources here. If more resources need to be tagged,
# we must add them in new extension.
subnet_def.COLLECTION_NAME: subnet_def.RESOURCE_NAME,
port_def.COLLECTION_NAME: port_def.RESOURCE_NAME,
subnetpool_def.COLLECTION_NAME: subnetpool_def.RESOURCE_NAME,
l3_apidef.ROUTERS: l3_apidef.ROUTER,
}
class TagExtController(tagging.TaggingController):
def __init__(self):
self.plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE)
self.supported_resources = TAG_SUPPORTED_RESOURCES
class Tag_ext(api_extensions.ExtensionDescriptor):
"""Extension class supporting tags for ext resources."""
@classmethod
def get_name(cls):
return ("Tag support for resources: %s"
% ', '.join(TAG_SUPPORTED_RESOURCES.values()))
@classmethod
def get_alias(cls):
return "tag-ext"
@classmethod
def get_description(cls):
return "Extends tag support to more L2 and L3 resources."
@classmethod
def get_updated(cls):
return "2017-01-01T00:00:00-00:00"
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
exts = []
action_status = {'index': 200, 'show': 204, 'update': 201,
'update_all': 200, 'delete': 204, 'delete_all': 204}
controller = api_resource.Resource(TagExtController(),
faults.FAULT_MAP,
action_status=action_status)
collection_methods = {"delete_all": "DELETE",
"update_all": "PUT"}
exts = []
for collection_name, member_name in TAG_SUPPORTED_RESOURCES.items():
parent = {'member_name': member_name,
'collection_name': collection_name}
exts.append(extensions.ResourceExtension(
tagging.TAGS, controller, parent,
collection_methods=collection_methods))
return exts
def get_optional_extensions(self):
return ['router']
def get_extended_resources(self, version):
if version != "2.0":
return {}
EXTENDED_ATTRIBUTES_2_0 = {}
for collection_name in TAG_SUPPORTED_RESOURCES:
EXTENDED_ATTRIBUTES_2_0[collection_name] = (
tagging.TAG_ATTRIBUTE_MAP)
return EXTENDED_ATTRIBUTES_2_0

View File

@ -33,24 +33,8 @@ TAG = 'tag'
TAGS = TAG + 's'
MAX_TAG_LEN = 60
TAG_PLUGIN_TYPE = 'TAG'
# Not support resources supported by tag, tag-ext
EXCEPTION_RESOURCES = ['networks', 'subnets', 'ports', 'subnetpools',
'routers']
# TODO(hichihara): This method is removed after tag, tag-ext extensions
# have been removed.
def get_tagging_supported_resources():
# Removes some resources supported by tag, tag-ext
parent_map = standard_attr.get_tag_resource_parent_map()
remove_resources = [res for res in parent_map
if res in EXCEPTION_RESOURCES]
for resource in remove_resources:
del parent_map[resource]
return parent_map
TAG_SUPPORTED_RESOURCES = get_tagging_supported_resources()
TAG_SUPPORTED_RESOURCES = standard_attr.get_tag_resource_parent_map()
TAG_ATTRIBUTE_MAP = {
TAGS: {'allow_post': False, 'allow_put': False, 'is_visible': True}
}
@ -187,9 +171,7 @@ class Tagging(api_extensions.ExtensionDescriptor):
return "2017-01-01T00:00:00-00:00"
def get_required_extensions(self):
# This is needed so that depending project easily moves from old
# extensions although this extension self can run without them.
return ['tag', 'tag-ext']
return []
@classmethod
def get_resources(cls):

View File

@ -37,7 +37,7 @@ resource_model_map = standard_attr.get_standard_attr_resource_model_map()
class TagPlugin(common_db_mixin.CommonDbMixin, tagging.TagPluginBase):
"""Implementation of the Neutron Tag Service Plugin."""
supported_extension_aliases = ['tag', 'tag-ext', 'standard-attr-tag']
supported_extension_aliases = ['standard-attr-tag']
def __new__(cls, *args, **kwargs):
inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs)

View File

@ -46,7 +46,5 @@ NETWORK_API_EXTENSIONS+=",standard-attr-revisions"
NETWORK_API_EXTENSIONS+=",standard-attr-timestamp"
NETWORK_API_EXTENSIONS+=",standard-attr-tag"
NETWORK_API_EXTENSIONS+=",subnet_allocation"
NETWORK_API_EXTENSIONS+=",tag"
NETWORK_API_EXTENSIONS+=",tag-ext"
NETWORK_API_EXTENSIONS+=",trunk"
NETWORK_API_EXTENSIONS+=",trunk-details"

View File

@ -1,406 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api import attributes
from neutron_lib import context
from oslo_utils import uuidutils
import testscenarios
from neutron.api import extensions
from neutron.common import config
import neutron.extensions
from neutron.objects.qos import policy
from neutron.objects import trunk
from neutron.services.tag import tag_plugin
from neutron.tests import fake_notifier
from neutron.tests.unit.extensions import test_l3
from neutron.tests.unit.extensions import test_securitygroup
DB_PLUGIN_KLASS = 'neutron.tests.unit.extensions.test_tag.TestTagPlugin'
load_tests = testscenarios.load_tests_apply_scenarios
extensions_path = ':'.join(neutron.extensions.__path__)
class TestTagPlugin(test_securitygroup.SecurityGroupTestPlugin,
test_l3.TestL3NatBasePlugin):
__native_pagination_support = True
__native_sorting_support = True
supported_extension_aliases = ["external-net", "security-group"]
class TestTagApiBase(test_securitygroup.SecurityGroupsTestCase,
test_l3.L3NatTestCaseMixin):
scenarios = [
('Network Tag Test',
dict(collection='networks',
member='network')),
('Subnet Tag Test',
dict(collection='subnets',
member='subnet')),
('Port Tag Test',
dict(collection='ports',
member='port')),
('Subnetpool Tag Test',
dict(collection='subnetpools',
member='subnetpool')),
('Router Tag Test',
dict(collection='routers',
member='router')),
('Floatingip Tag Test',
dict(collection='floatingips',
member='floatingip')),
('Securitygroup Tag Test',
dict(collection='security-groups',
member='security_group')),
('QoS Policy Tag Test',
dict(collection='policies',
member='policy')),
('Trunk Tag Test',
dict(collection='trunks',
member='trunk')),
]
def setUp(self):
service_plugins = {
'TAG': "neutron.services.tag.tag_plugin.TagPlugin",
'router':
"neutron.tests.unit.extensions.test_l3.TestL3NatServicePlugin"}
super(TestTagApiBase, self).setUp(plugin=DB_PLUGIN_KLASS,
service_plugins=service_plugins)
plugin = tag_plugin.TagPlugin()
l3_plugin = test_l3.TestL3NatServicePlugin()
sec_plugin = test_securitygroup.SecurityGroupTestPlugin()
ext_mgr = extensions.PluginAwareExtensionManager(
extensions_path, {'router': l3_plugin, 'TAG': plugin,
'sec': sec_plugin}
)
ext_mgr.extend_resources("2.0", attributes.RESOURCES)
app = config.load_paste_app('extensions_test_app')
self.ext_api = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
def _is_object(self):
return self.collection in ['policies', 'trunks']
def _prepare_make_resource(self):
if self.collection == "floatingips":
net = self._make_network(self.fmt, 'net1', True)
self._set_net_external(net['network']['id'])
self._make_subnet(self.fmt, net, '10.0.0.1', '10.0.0.0/24')
info = {'network_id': net['network']['id']}
self._make_router(self.fmt, None,
external_gateway_info=info)
self.net = net['network']
def _make_object(self):
ctxt = context.get_admin_context()
if self.collection == "policies":
self.obj = policy.QosPolicy(context=ctxt,
id=uuidutils.generate_uuid(),
project_id='tenant', name='pol1',
rules=[])
elif self.collection == "trunks":
net = self._make_network(self.fmt, 'net1', True)
port = self._make_port(self.fmt, net['network']['id'])
self.obj = trunk.Trunk(context=ctxt,
id=uuidutils.generate_uuid(),
project_id='tenant', name='',
port_id=port['port']['id'])
self.obj.create()
return self.obj.id
def _make_resource(self):
if self._is_object():
return self._make_object()
if self.collection == "networks":
res = self._make_network(self.fmt, 'net1', True)
elif self.collection == "subnets":
net = self._make_network(self.fmt, 'net1', True)
res = self._make_subnet(self.fmt, net, '10.0.0.1', '10.0.0.0/24')
elif self.collection == "ports":
net = self._make_network(self.fmt, 'net1', True)
res = self._make_port(self.fmt, net['network']['id'])
elif self.collection == "subnetpools":
res = self._make_subnetpool(self.fmt, ['10.0.0.0/8'],
name='my pool', tenant_id="tenant")
elif self.collection == "routers":
res = self._make_router(self.fmt, None)
elif self.collection == "floatingips":
res = self._make_floatingip(self.fmt, self.net['id'])
elif self.collection == "security-groups":
res = self._make_security_group(self.fmt, 'sec1', '')
return res[self.member]['id']
def _get_object_tags(self):
ctxt = context.get_admin_context()
res = self.obj.get_object(ctxt, id=self.resource_id)
return res.to_dict()['tags']
def _get_resource_tags(self):
if self._is_object():
return self._get_object_tags()
res = self._show(self.collection, self.resource_id)
return res[self.member]['tags']
def _put_tag(self, tag):
req = self._req('PUT', self.collection, id=self.resource_id,
subresource='tags', sub_id=tag)
return req.get_response(self.ext_api)
def _put_tags(self, tags=None, body=None):
if tags:
body = {'tags': tags}
elif body:
body = body
else:
body = {}
req = self._req('PUT', self.collection, data=body, id=self.resource_id,
subresource='tags')
return req.get_response(self.ext_api)
def _get_tag(self, tag):
req = self._req('GET', self.collection, id=self.resource_id,
subresource='tags', sub_id=tag)
return req.get_response(self.ext_api)
def _delete_tag(self, tag):
req = self._req('DELETE', self.collection, id=self.resource_id,
subresource='tags', sub_id=tag)
return req.get_response(self.ext_api)
def _delete_tags(self):
req = self._req('DELETE', self.collection, id=self.resource_id,
subresource='tags')
return req.get_response(self.ext_api)
def _assertEqualTags(self, expected, actual):
self.assertEqual(set(expected), set(actual))
def _get_tags_filter_objects(self, tags, tags_any, not_tags,
not_tags_any):
filters = {}
if tags:
filters['tags'] = tags
if tags_any:
filters['tags-any'] = tags_any
if not_tags:
filters['not-tags'] = not_tags
if not_tags_any:
filters['not-tags-any'] = not_tags_any
if self.collection == "policies":
obj_class = policy.QosPolicy
elif self.collection == "trunks":
obj_class = trunk.Trunk
ctxt = context.get_admin_context()
res = obj_class.get_objects(ctxt, **filters)
return [n.id for n in res]
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):
if self._is_object():
return self._get_tags_filter_objects(tags, tags_any, not_tags,
not_tags_any)
params = self._make_query_string(tags, tags_any, not_tags,
not_tags_any)
res = self._list(self.collection, query_params=params)
return [n['id'] for n in res[self.collection.replace('-', '_')]]
def _test_notification_report(self, expect_notify):
notify = set(n['event_type'] for n in fake_notifier.NOTIFICATIONS)
duplicated_notify = expect_notify & notify
self.assertEqual(expect_notify, duplicated_notify)
fake_notifier.reset()
class TestResourceTagApi(TestTagApiBase):
def setUp(self):
super(TestResourceTagApi, self).setUp()
self._prepare_make_resource()
self.resource_id = self._make_resource()
def test_put_tag(self):
expect_notify = set(['tag.create.start',
'tag.create.end'])
res = self._put_tag('red')
self.assertEqual(201, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['red'], tags)
self._test_notification_report(expect_notify)
res = self._put_tag('blue')
self.assertEqual(201, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['red', 'blue'], tags)
self._test_notification_report(expect_notify)
def test_put_tag_exists(self):
res = self._put_tag('blue')
self.assertEqual(201, res.status_int)
res = self._put_tag('blue')
self.assertEqual(201, res.status_int)
def test_put_tags(self):
expect_notify = set(['tag.update.start',
'tag.update.end'])
res = self._put_tags(['red', 'green'])
self.assertEqual(200, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['red', 'green'], tags)
self._test_notification_report(expect_notify)
def test_put_invalid_tags(self):
res = self._put_tags()
self.assertEqual(400, res.status_int)
res = self._put_tags(body=7)
self.assertEqual(400, res.status_int)
res = self._put_tags(body={'invalid': True})
self.assertEqual(400, res.status_int)
def test_put_tags_replace(self):
res = self._put_tags(['red', 'green'])
self.assertEqual(200, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['red', 'green'], tags)
res = self._put_tags(['blue', 'red'])
self.assertEqual(200, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['blue', 'red'], tags)
def test_get_tag(self):
res = self._put_tag('red')
self.assertEqual(201, res.status_int)
res = self._get_tag('red')
self.assertEqual(204, res.status_int)
def test_get_tag_notfound(self):
res = self._put_tag('red')
self.assertEqual(201, res.status_int)
res = self._get_tag('green')
self.assertEqual(404, res.status_int)
def test_delete_tag(self):
expect_notify = set(['tag.delete.start',
'tag.delete.end'])
res = self._put_tags(['red', 'green'])
self.assertEqual(200, res.status_int)
res = self._delete_tag('red')
self.assertEqual(204, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags(['green'], tags)
self._test_notification_report(expect_notify)
def test_delete_tag_notfound(self):
res = self._put_tags(['red', 'green'])
self.assertEqual(200, res.status_int)
res = self._delete_tag('blue')
self.assertEqual(404, res.status_int)
def test_delete_tags(self):
expect_notify = set(['tag.delete_all.start',
'tag.delete_all.end'])
res = self._put_tags(['red', 'green'])
self.assertEqual(200, res.status_int)
res = self._delete_tags()
self.assertEqual(204, res.status_int)
tags = self._get_resource_tags()
self._assertEqualTags([], tags)
self._test_notification_report(expect_notify)
class TestResourceTagFilter(TestTagApiBase):
def setUp(self):
super(TestResourceTagFilter, self).setUp()
self._prepare_resource_tags()
def _make_tags(self, resource_id, tags):
body = {'tags': tags}
req = self._req('PUT', self.collection, data=body, id=resource_id,
subresource='tags')
return req.get_response(self.ext_api)
def _prepare_resource_tags(self):
self._prepare_make_resource()
self.res1 = self._make_resource()
self.res2 = self._make_resource()
self.res3 = self._make_resource()
self.res4 = self._make_resource()
self.res5 = self._make_resource()
self.res_ids = [self.res1, self.res2, self.res3, self.res4, self.res5]
self._make_tags(self.res1, ['red'])
self._make_tags(self.res2, ['red', 'blue'])
self._make_tags(self.res3, ['red', 'blue', 'green'])
self._make_tags(self.res4, ['green'])
# res5: no tags
def _assertEqualResources(self, expected, resources):
actual = [n for n in resources if n in self.res_ids]
self.assertEqual(set(expected), set(actual))
def test_filter_tags_single(self):
resources = self._get_tags_filter_resources(tags=['red'])
self._assertEqualResources([self.res1, self.res2, self.res3],
resources)
def test_filter_tags_multi(self):
resources = self._get_tags_filter_resources(tags=['red', 'blue'])
self._assertEqualResources([self.res2, self.res3], resources)
def test_filter_tags_any_single(self):
resources = self._get_tags_filter_resources(tags_any=['blue'])
self._assertEqualResources([self.res2, self.res3], resources)
def test_filter_tags_any_multi(self):
resources = self._get_tags_filter_resources(tags_any=['red', 'blue'])
self._assertEqualResources([self.res1, self.res2, self.res3],
resources)
def test_filter_not_tags_single(self):
resources = self._get_tags_filter_resources(not_tags=['red'])
self._assertEqualResources([self.res4, self.res5], resources)
def test_filter_not_tags_multi(self):
resources = self._get_tags_filter_resources(not_tags=['red', 'blue'])
self._assertEqualResources([self.res1, self.res4, self.res5],
resources)
def test_filter_not_tags_any_single(self):
resources = self._get_tags_filter_resources(not_tags_any=['blue'])
self._assertEqualResources([self.res1, self.res4, self.res5],
resources)
def test_filter_not_tags_any_multi(self):
resources = self._get_tags_filter_resources(not_tags_any=['red',
'blue'])
self._assertEqualResources([self.res4, self.res5], resources)