From 96f0142b8089a85f1a031f236c6d39fd463bf86c Mon Sep 17 00:00:00 2001 From: Hirofumi Ichihara Date: Fri, 16 Jun 2017 16:46:37 +0900 Subject: [PATCH] Tag mechanism supports resources with standard attribute Tag mechanism supports network, subnet, port, subnetpool router resources only. This patch allow tag mechanism to support resources with standard attribute. Two old extenions are kept because of backward compatibility. They will be removed in Queens release. APIImpact: Tag is supported by resources with standard attribute DocImpact: allow users to set tags on resources with standard attribute Change-Id: Id7bb13b5beb58c313eea94ca03835d3daf5c94bc Closes-Bug: #1682775 --- doc/source/contributor/internals/db_layer.rst | 5 + doc/source/contributor/internals/tag.rst | 25 +- neutron/db/models/l3.py | 4 + neutron/db/models/securitygroup.py | 2 + neutron/db/models_v2.py | 11 + neutron/db/qos/models.py | 2 + neutron/db/standard_attr.py | 30 +- neutron/extensions/tag.py | 168 +----------- neutron/extensions/tag_ext.py | 13 +- neutron/extensions/tagging.py | 258 ++++++++++++++++++ neutron/objects/base.py | 1 + .../objects/extensions/standardattributes.py | 7 + neutron/objects/qos/policy.py | 11 + neutron/objects/trunk.py | 11 + neutron/services/tag/tag_plugin.py | 32 +-- neutron/services/trunk/models.py | 2 + .../tests/contrib/hooks/api_all_extensions | 1 + neutron/tests/unit/db/test_standard_attr.py | 46 ++++ neutron/tests/unit/extensions/test_tag.py | 189 ++++++++++--- .../extensions/test_standardattributes.py | 2 + ...andardattr-resources-6f757cb39cc1dcfe.yaml | 10 + 21 files changed, 596 insertions(+), 234 deletions(-) create mode 100644 neutron/extensions/tagging.py create mode 100644 releasenotes/notes/add-tag-all-standardattr-resources-6f757cb39cc1dcfe.yaml diff --git a/doc/source/contributor/internals/db_layer.rst b/doc/source/contributor/internals/db_layer.rst index 6ecfc27122a..67940c0c054 100644 --- a/doc/source/contributor/internals/db_layer.rst +++ b/doc/source/contributor/internals/db_layer.rst @@ -87,6 +87,11 @@ may appear under. In most cases, this will only be one (e.g. 'ports' for the Port model). This is used by all of the service plugins that add standard attribute fields to determine which API responses need to be populated. +A model that supports tag mechanism must implement the property +'collection_resource_map' which is a dict of 'collection_name' and +'resource_name' for API resources. And also the model must implement +'tag_support' with a value True. + The introduction of a new standard attribute only requires one column addition to the 'standardattribute' table for one-to-one relationships or a new table for one-to-many or one-to-zero relationships. Then all of the models using the diff --git a/doc/source/contributor/internals/tag.rst b/doc/source/contributor/internals/tag.rst index 6bb199ef594..18d9acb92bf 100644 --- a/doc/source/contributor/internals/tag.rst +++ b/doc/source/contributor/internals/tag.rst @@ -50,9 +50,28 @@ Which Resources --------------- Tag system uses standardattr mechanism so it's targeting to resources that have -the mechanism. The system is provided by 'tag' extension and 'tag-ext' -extension. The 'tag' extension supports networks only. The 'tag-ext' extension -supports subnets, ports, routers, and subnet pools. +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 +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 +must include a release note for API user. + +Current API resources extended by tag extensions: + +- floatingips +- networks +- policies +- ports +- routers +- security_groups +- subnetpools +- subnets +- trunks Model ----- diff --git a/neutron/db/models/l3.py b/neutron/db/models/l3.py index 9f19ebab8dd..70d751ef78d 100644 --- a/neutron/db/models/l3.py +++ b/neutron/db/models/l3.py @@ -63,6 +63,8 @@ class Router(standard_attr.HasStandardAttributes, model_base.BASEV2, 'Agent', lazy='subquery', viewonly=True, secondary=rb_model.RouterL3AgentBinding.__table__) api_collections = [l3.ROUTERS] + collection_resource_map = {l3.ROUTERS: l3.ROUTER} + tag_support = True class FloatingIP(standard_attr.HasStandardAttributes, model_base.BASEV2, @@ -103,6 +105,8 @@ class FloatingIP(standard_attr.HasStandardAttributes, model_base.BASEV2, '0fixedportid0fixedipaddress')), model_base.BASEV2.__table_args__,) api_collections = [l3.FLOATINGIPS] + collection_resource_map = {l3.FLOATINGIPS: l3.FLOATINGIP} + tag_support = True class RouterRoute(model_base.BASEV2, models_v2.Route): diff --git a/neutron/db/models/securitygroup.py b/neutron/db/models/securitygroup.py index 894d6f83485..8781d9fc42b 100644 --- a/neutron/db/models/securitygroup.py +++ b/neutron/db/models/securitygroup.py @@ -28,6 +28,8 @@ class SecurityGroup(standard_attr.HasStandardAttributes, model_base.BASEV2, name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) api_collections = [sg.SECURITYGROUPS] + collection_resource_map = {sg.SECURITYGROUPS: 'security_group'} + tag_support = True class DefaultSecurityGroup(model_base.BASEV2, model_base.HasProjectPrimaryKey): diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index 579ee840ddd..4a09f1ab6a0 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -110,6 +110,9 @@ class Port(standard_attr.HasStandardAttributes, model_base.BASEV2, model_base.BASEV2.__table_args__ ) api_collections = [port_def.COLLECTION_NAME] + collection_resource_map = {port_def.COLLECTION_NAME: + port_def.RESOURCE_NAME} + tag_support = True def __init__(self, id=None, tenant_id=None, project_id=None, name=None, network_id=None, mac_address=None, admin_state_up=None, @@ -202,6 +205,9 @@ class Subnet(standard_attr.HasStandardAttributes, model_base.BASEV2, foreign_keys='Subnet.network_id', primaryjoin='Subnet.network_id==NetworkRBAC.object_id') api_collections = [subnet_def.COLLECTION_NAME] + collection_resource_map = {subnet_def.COLLECTION_NAME: + subnet_def.RESOURCE_NAME} + tag_support = True class SubnetPoolPrefix(model_base.BASEV2): @@ -239,6 +245,9 @@ class SubnetPool(standard_attr.HasStandardAttributes, model_base.BASEV2, cascade='all, delete, delete-orphan', lazy='subquery') api_collections = [subnetpool_def.COLLECTION_NAME] + collection_resource_map = {subnetpool_def.COLLECTION_NAME: + subnetpool_def.RESOURCE_NAME} + tag_support = True class Network(standard_attr.HasStandardAttributes, model_base.BASEV2, @@ -260,3 +269,5 @@ class Network(standard_attr.HasStandardAttributes, model_base.BASEV2, 'Agent', lazy='subquery', viewonly=True, secondary=ndab_model.NetworkDhcpAgentBinding.__table__) api_collections = [net_def.COLLECTION_NAME] + collection_resource_map = {net_def.COLLECTION_NAME: net_def.RESOURCE_NAME} + tag_support = True diff --git a/neutron/db/qos/models.py b/neutron/db/qos/models.py index f279e23182a..af7271902ea 100644 --- a/neutron/db/qos/models.py +++ b/neutron/db/qos/models.py @@ -31,6 +31,8 @@ class QosPolicy(standard_attr.HasStandardAttributes, model_base.BASEV2, backref='qos_policy', lazy='subquery', cascade='all, delete, delete-orphan') api_collections = ['policies'] + collection_resource_map = {'policies': 'policy'} + tag_support = True class QosNetworkPolicyBinding(model_base.BASEV2): diff --git a/neutron/db/standard_attr.py b/neutron/db/standard_attr.py index a6e1c6fdb3d..7e20d7759a8 100644 --- a/neutron/db/standard_attr.py +++ b/neutron/db/standard_attr.py @@ -21,7 +21,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext import declarative from sqlalchemy.orm import session as se -from neutron._i18n import _LE +from neutron._i18n import _, _LE from neutron.db import sqlalchemytypes @@ -98,6 +98,18 @@ class HasStandardAttributes(object): return cls.api_collections raise NotImplementedError("%s must define api_collections" % cls) + @classmethod + def get_collection_resource_map(cls): + try: + return cls.collection_resource_map + except AttributeError: + raise NotImplementedError("%s must define " + "collection_resource_map" % cls) + + @classmethod + def validate_tag_support(cls): + return getattr(cls, 'tag_support', False) + @declarative.declared_attr def standard_attr_id(cls): return sa.Column( @@ -175,6 +187,22 @@ def get_standard_attr_resource_model_map(): return rs_map +def get_tag_resource_parent_map(): + parent_map = {} + for subclass in HasStandardAttributes.__subclasses__(): + if subclass.validate_tag_support(): + for collection, resource in (subclass.get_collection_resource_map() + .items()): + if collection in parent_map: + msg = (_("API parent %(collection)s/%(resource)s for " + "model %(subclass)s is already registered.") % + dict(collection=collection, resource=resource, + subclass=subclass)) + raise RuntimeError(msg) + parent_map[collection] = resource + return parent_map + + @event.listens_for(se.Session, 'after_bulk_delete') def throw_exception_on_bulk_delete_of_listened_for_objects(delete_context): if hasattr(delete_context.mapper.class_, 'revises_on_change'): diff --git a/neutron/extensions/tag.py b/neutron/extensions/tag.py index d8f3a9b3d4a..56da7ff20c2 100644 --- a/neutron/extensions/tag.py +++ b/neutron/extensions/tag.py @@ -11,28 +11,17 @@ # License for the specific language governing permissions and limitations # under the License. -import abc - from neutron_lib.api.definitions import network from neutron_lib.api import extensions as api_extensions -from neutron_lib.api import validators -from neutron_lib import exceptions from neutron_lib.plugins import directory -from neutron_lib.services import base as service_base -import six -import webob.exc -from neutron._i18n import _ from neutron.api import extensions from neutron.api.v2 import base from neutron.api.v2 import resource as api_resource -from neutron.common import rpc as n_rpc +from neutron.extensions import tagging -TAG = 'tag' -TAGS = TAG + 's' -MAX_TAG_LEN = 60 -TAG_PLUGIN_TYPE = 'TAG' +# 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, @@ -40,120 +29,12 @@ TAG_SUPPORTED_RESOURCES = { network.COLLECTION_NAME: network.RESOURCE_NAME, } -TAG_ATTRIBUTE_MAP = { - TAGS: {'allow_post': False, 'allow_put': False, 'is_visible': True} -} - -class TagResourceNotFound(exceptions.NotFound): - message = _("Resource %(resource)s %(resource_id)s could not be found.") - - -class TagNotFound(exceptions.NotFound): - message = _("Tag %(tag)s could not be found.") - - -def validate_tag(tag): - msg = validators.validate_string(tag, MAX_TAG_LEN) - if msg: - raise exceptions.InvalidInput(error_message=msg) - - -def validate_tags(body): - if 'tags' not in body: - raise exceptions.InvalidInput(error_message=_("Invalid tags body")) - msg = validators.validate_list_of_unique_strings(body['tags'], MAX_TAG_LEN) - if msg: - raise exceptions.InvalidInput(error_message=msg) - - -def notify_tag_action(context, action, parent, parent_id, tags=None): - notifier = n_rpc.get_notifier('network') - tag_event = 'tag.%s' % action - # TODO(hichihara): Add 'updated_at' into payload - payload = {'parent_resource': parent, - 'parent_resource_id': parent_id} - if tags is not None: - payload['tags'] = tags - notifier.info(context, tag_event, payload) - - -class TagController(object): +class TagController(tagging.TaggingController): def __init__(self): - self.plugin = directory.get_plugin(TAG_PLUGIN_TYPE) + self.plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE) self.supported_resources = TAG_SUPPORTED_RESOURCES - def _get_parent_resource_and_id(self, kwargs): - for key in kwargs: - for resource in self.supported_resources: - if key == self.supported_resources[resource] + '_id': - return resource, kwargs[key] - return None, None - - def index(self, request, **kwargs): - # GET /v2.0/networks/{network_id}/tags - parent, parent_id = self._get_parent_resource_and_id(kwargs) - return self.plugin.get_tags(request.context, parent, parent_id) - - def show(self, request, id, **kwargs): - # GET /v2.0/networks/{network_id}/tags/{tag} - # id == tag - validate_tag(id) - parent, parent_id = self._get_parent_resource_and_id(kwargs) - return self.plugin.get_tag(request.context, parent, parent_id, id) - - def create(self, request, **kwargs): - # not supported - # POST /v2.0/networks/{network_id}/tags - raise webob.exc.HTTPNotFound("not supported") - - def update(self, request, id, **kwargs): - # PUT /v2.0/networks/{network_id}/tags/{tag} - # id == tag - validate_tag(id) - parent, parent_id = self._get_parent_resource_and_id(kwargs) - notify_tag_action(request.context, 'create.start', - parent, parent_id, [id]) - result = self.plugin.update_tag(request.context, parent, parent_id, id) - notify_tag_action(request.context, 'create.end', - parent, parent_id, [id]) - return result - - def update_all(self, request, body, **kwargs): - # PUT /v2.0/networks/{network_id}/tags - # body: {"tags": ["aaa", "bbb"]} - validate_tags(body) - parent, parent_id = self._get_parent_resource_and_id(kwargs) - notify_tag_action(request.context, 'update.start', - parent, parent_id, body['tags']) - result = self.plugin.update_tags(request.context, parent, - parent_id, body) - notify_tag_action(request.context, 'update.end', - parent, parent_id, body['tags']) - return result - - def delete(self, request, id, **kwargs): - # DELETE /v2.0/networks/{network_id}/tags/{tag} - # id == tag - validate_tag(id) - parent, parent_id = self._get_parent_resource_and_id(kwargs) - notify_tag_action(request.context, 'delete.start', - parent, parent_id, [id]) - result = self.plugin.delete_tag(request.context, parent, parent_id, id) - notify_tag_action(request.context, 'delete.end', - parent, parent_id, [id]) - return result - - def delete_all(self, request, **kwargs): - # DELETE /v2.0/networks/{network_id}/tags - parent, parent_id = self._get_parent_resource_and_id(kwargs) - notify_tag_action(request.context, 'delete_all.start', - parent, parent_id) - result = self.plugin.delete_tags(request.context, parent, parent_id) - notify_tag_action(request.context, 'delete_all.end', - parent, parent_id) - return result - class Tag(api_extensions.ExtensionDescriptor): """Extension class supporting tags.""" @@ -190,7 +71,7 @@ class Tag(api_extensions.ExtensionDescriptor): parent = {'member_name': member_name, 'collection_name': collection_name} exts.append(extensions.ResourceExtension( - TAGS, controller, parent, + tagging.TAGS, controller, parent, collection_methods=collection_methods)) return exts @@ -199,41 +80,6 @@ class Tag(api_extensions.ExtensionDescriptor): return {} EXTENDED_ATTRIBUTES_2_0 = {} for collection_name in TAG_SUPPORTED_RESOURCES: - EXTENDED_ATTRIBUTES_2_0[collection_name] = TAG_ATTRIBUTE_MAP + EXTENDED_ATTRIBUTES_2_0[collection_name] = ( + tagging.TAG_ATTRIBUTE_MAP) return EXTENDED_ATTRIBUTES_2_0 - - -@six.add_metaclass(abc.ABCMeta) -class TagPluginBase(service_base.ServicePluginBase): - """REST API to operate the Tag.""" - - def get_plugin_description(self): - return "Tag support" - - @classmethod - def get_plugin_type(cls): - return TAG_PLUGIN_TYPE - - @abc.abstractmethod - def get_tags(self, context, resource, resource_id): - pass - - @abc.abstractmethod - def get_tag(self, context, resource, resource_id, tag): - pass - - @abc.abstractmethod - def update_tags(self, context, resource, resource_id, body): - pass - - @abc.abstractmethod - def update_tag(self, context, resource, resource_id, tag): - pass - - @abc.abstractmethod - def delete_tags(self, context, resource, resource_id): - pass - - @abc.abstractmethod - def delete_tag(self, context, resource, resource_id, tag): - pass diff --git a/neutron/extensions/tag_ext.py b/neutron/extensions/tag_ext.py index 88d2c0aa48a..ff1f26b59f8 100644 --- a/neutron/extensions/tag_ext.py +++ b/neutron/extensions/tag_ext.py @@ -21,7 +21,10 @@ from neutron.api import extensions from neutron.api.v2 import base from neutron.api.v2 import resource as api_resource from neutron.extensions import l3 -from neutron.extensions import tag as tag_base +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, @@ -33,9 +36,9 @@ TAG_SUPPORTED_RESOURCES = { } -class TagExtController(tag_base.TagController): +class TagExtController(tagging.TaggingController): def __init__(self): - self.plugin = directory.get_plugin(tag_base.TAG_PLUGIN_TYPE) + self.plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE) self.supported_resources = TAG_SUPPORTED_RESOURCES @@ -75,7 +78,7 @@ class Tag_ext(api_extensions.ExtensionDescriptor): parent = {'member_name': member_name, 'collection_name': collection_name} exts.append(extensions.ResourceExtension( - tag_base.TAGS, controller, parent, + tagging.TAGS, controller, parent, collection_methods=collection_methods)) return exts @@ -88,5 +91,5 @@ class Tag_ext(api_extensions.ExtensionDescriptor): EXTENDED_ATTRIBUTES_2_0 = {} for collection_name in TAG_SUPPORTED_RESOURCES: EXTENDED_ATTRIBUTES_2_0[collection_name] = ( - tag_base.TAG_ATTRIBUTE_MAP) + tagging.TAG_ATTRIBUTE_MAP) return EXTENDED_ATTRIBUTES_2_0 diff --git a/neutron/extensions/tagging.py b/neutron/extensions/tagging.py new file mode 100644 index 00000000000..24ab3e645dc --- /dev/null +++ b/neutron/extensions/tagging.py @@ -0,0 +1,258 @@ +# +# 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. + +import abc + +from neutron_lib.api import extensions as api_extensions +from neutron_lib.api import validators +from neutron_lib import exceptions +from neutron_lib.plugins import directory +from neutron_lib.services import base as service_base +import six +import webob.exc + +from neutron._i18n import _ +from neutron.api import extensions +from neutron.api.v2 import base +from neutron.api.v2 import resource as api_resource +from neutron.common import rpc as n_rpc +from neutron.db import standard_attr + + +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_ATTRIBUTE_MAP = { + TAGS: {'allow_post': False, 'allow_put': False, 'is_visible': True} +} + + +class TagResourceNotFound(exceptions.NotFound): + message = _("Resource %(resource)s %(resource_id)s could not be found.") + + +class TagNotFound(exceptions.NotFound): + message = _("Tag %(tag)s could not be found.") + + +def validate_tag(tag): + msg = validators.validate_string(tag, MAX_TAG_LEN) + if msg: + raise exceptions.InvalidInput(error_message=msg) + + +def validate_tags(body): + if 'tags' not in body: + raise exceptions.InvalidInput(error_message=_("Invalid tags body")) + msg = validators.validate_list_of_unique_strings(body['tags'], MAX_TAG_LEN) + if msg: + raise exceptions.InvalidInput(error_message=msg) + + +def notify_tag_action(context, action, parent, parent_id, tags=None): + notifier = n_rpc.get_notifier('network') + tag_event = 'tag.%s' % action + # TODO(hichihara): Add 'updated_at' into payload + payload = {'parent_resource': parent, + 'parent_resource_id': parent_id} + if tags is not None: + payload['tags'] = tags + notifier.info(context, tag_event, payload) + + +class TaggingController(object): + def __init__(self): + self.plugin = directory.get_plugin(TAG_PLUGIN_TYPE) + self.supported_resources = TAG_SUPPORTED_RESOURCES + + def _get_parent_resource_and_id(self, kwargs): + for key in kwargs: + for resource in self.supported_resources: + if key == self.supported_resources[resource] + '_id': + return resource, kwargs[key] + return None, None + + def index(self, request, **kwargs): + # GET /v2.0/networks/{network_id}/tags + parent, parent_id = self._get_parent_resource_and_id(kwargs) + return self.plugin.get_tags(request.context, parent, parent_id) + + def show(self, request, id, **kwargs): + # GET /v2.0/networks/{network_id}/tags/{tag} + # id == tag + validate_tag(id) + parent, parent_id = self._get_parent_resource_and_id(kwargs) + return self.plugin.get_tag(request.context, parent, parent_id, id) + + def create(self, request, **kwargs): + # not supported + # POST /v2.0/networks/{network_id}/tags + raise webob.exc.HTTPNotFound("not supported") + + def update(self, request, id, **kwargs): + # PUT /v2.0/networks/{network_id}/tags/{tag} + # id == tag + validate_tag(id) + parent, parent_id = self._get_parent_resource_and_id(kwargs) + notify_tag_action(request.context, 'create.start', + parent, parent_id, [id]) + result = self.plugin.update_tag(request.context, parent, parent_id, id) + notify_tag_action(request.context, 'create.end', + parent, parent_id, [id]) + return result + + def update_all(self, request, body, **kwargs): + # PUT /v2.0/networks/{network_id}/tags + # body: {"tags": ["aaa", "bbb"]} + validate_tags(body) + parent, parent_id = self._get_parent_resource_and_id(kwargs) + notify_tag_action(request.context, 'update.start', + parent, parent_id, body['tags']) + result = self.plugin.update_tags(request.context, parent, + parent_id, body) + notify_tag_action(request.context, 'update.end', + parent, parent_id, body['tags']) + return result + + def delete(self, request, id, **kwargs): + # DELETE /v2.0/networks/{network_id}/tags/{tag} + # id == tag + validate_tag(id) + parent, parent_id = self._get_parent_resource_and_id(kwargs) + notify_tag_action(request.context, 'delete.start', + parent, parent_id, [id]) + result = self.plugin.delete_tag(request.context, parent, parent_id, id) + notify_tag_action(request.context, 'delete.end', + parent, parent_id, [id]) + return result + + def delete_all(self, request, **kwargs): + # DELETE /v2.0/networks/{network_id}/tags + parent, parent_id = self._get_parent_resource_and_id(kwargs) + notify_tag_action(request.context, 'delete_all.start', + parent, parent_id) + result = self.plugin.delete_tags(request.context, parent, parent_id) + notify_tag_action(request.context, 'delete_all.end', + parent, parent_id) + return result + + +class Tagging(api_extensions.ExtensionDescriptor): + """Extension class supporting tags.""" + + @classmethod + def get_name(cls): + return ("Tag support for resources with standard attribute: %s" + % ', '.join(TAG_SUPPORTED_RESOURCES.values())) + + @classmethod + def get_alias(cls): + return "standard-attr-tag" + + @classmethod + def get_description(cls): + return "Enables to set tag on resources with standard attribute." + + @classmethod + def get_updated(cls): + 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'] + + @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(TaggingController(), + base.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(): + if 'security_group' in collection_name: + collection_name = collection_name.replace('_', '-') + parent = {'member_name': member_name, + 'collection_name': collection_name} + exts.append(extensions.ResourceExtension( + 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] = TAG_ATTRIBUTE_MAP + return EXTENDED_ATTRIBUTES_2_0 + + +@six.add_metaclass(abc.ABCMeta) +class TagPluginBase(service_base.ServicePluginBase): + """REST API to operate the Tag.""" + + def get_plugin_description(self): + return "Tag support" + + @classmethod + def get_plugin_type(cls): + return TAG_PLUGIN_TYPE + + @abc.abstractmethod + def get_tags(self, context, resource, resource_id): + pass + + @abc.abstractmethod + def get_tag(self, context, resource, resource_id, tag): + pass + + @abc.abstractmethod + def update_tags(self, context, resource, resource_id, body): + pass + + @abc.abstractmethod + def update_tag(self, context, resource, resource_id, tag): + pass + + @abc.abstractmethod + def delete_tags(self, context, resource, resource_id): + pass + + @abc.abstractmethod + def delete_tag(self, context, resource, resource_id, tag): + pass diff --git a/neutron/objects/base.py b/neutron/objects/base.py index 8815430a3b5..584dc6f79c1 100644 --- a/neutron/objects/base.py +++ b/neutron/objects/base.py @@ -283,6 +283,7 @@ class DeclarativeObject(abc.ABCMeta): property(lambda x: x.db_obj.standard_attr_id if x.db_obj else None)) standardattributes.add_standard_attributes(cls) + standardattributes.add_tag_filter_names(cls) # Instantiate extra filters per class cls.extra_filter_names = set(cls.extra_filter_names) # add tenant_id filter for objects that have project_id diff --git a/neutron/objects/extensions/standardattributes.py b/neutron/objects/extensions/standardattributes.py index f2e4ed6c042..be9c9639299 100644 --- a/neutron/objects/extensions/standardattributes.py +++ b/neutron/objects/extensions/standardattributes.py @@ -30,3 +30,10 @@ def add_standard_attributes(cls): # revision numbers are managed by service plugin and are bumped # automatically; consumers should not bump them explicitly cls.fields_no_update.append('revision_number') + + +def add_tag_filter_names(cls): + cls.add_extra_filter_name("tags") + cls.add_extra_filter_name("not-tags") + cls.add_extra_filter_name("tags-any") + cls.add_extra_filter_name("not-tags-any") diff --git a/neutron/objects/qos/policy.py b/neutron/objects/qos/policy.py index ca65456b2cc..567512c3412 100644 --- a/neutron/objects/qos/policy.py +++ b/neutron/objects/qos/policy.py @@ -102,6 +102,17 @@ class QosPolicy(rbac_db.NeutronRbacObject): raise exceptions.QosRuleNotFound(policy_id=self.id, rule_id=rule_id) + # TODO(hichihara): For tag mechanism. This will be removed in bug/1704137 + def to_dict(self): + _dict = super(QosPolicy, self).to_dict() + try: + _dict['tags'] = [t.tag for t in self.db_obj.standard_attr.tags] + except AttributeError: + # AttrtibuteError can be raised when accessing self.db_obj + # or self.db_obj.standard_attr + pass + return _dict + @classmethod def get_object(cls, context, **kwargs): # We want to get the policy regardless of its tenant id. We'll make diff --git a/neutron/objects/trunk.py b/neutron/objects/trunk.py index d07212a99dd..b72bb551b1f 100644 --- a/neutron/objects/trunk.py +++ b/neutron/objects/trunk.py @@ -126,6 +126,17 @@ class Trunk(base.NeutronDbObject): self.update_fields(kwargs) super(Trunk, self).update() + # TODO(hichihara): For tag mechanism. This will be removed in bug/1704137 + def to_dict(self): + _dict = super(Trunk, self).to_dict() + try: + _dict['tags'] = [t.tag for t in self.db_obj.standard_attr.tags] + except AttributeError: + # AttrtibuteError can be raised when accessing self.db_obj + # or self.db_obj.standard_attr + pass + return _dict + def obj_make_compatible(self, primitive, target_version): _target_version = versionutils.convert_version_to_tuple(target_version) diff --git a/neutron/services/tag/tag_plugin.py b/neutron/services/tag/tag_plugin.py index afeae84b789..a52d4a9270a 100644 --- a/neutron/services/tag/tag_plugin.py +++ b/neutron/services/tag/tag_plugin.py @@ -14,10 +14,6 @@ import functools -from neutron_lib.api.definitions import network as net_def -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.plugins import directory from oslo_log import helpers as log_helpers from sqlalchemy.orm import exc @@ -26,32 +22,22 @@ from neutron.db import _model_query as model_query from neutron.db import _resource_extend as resource_extend from neutron.db import api as db_api from neutron.db import common_db_mixin -from neutron.db.models import l3 as l3_model -from neutron.db import models_v2 +from neutron.db import standard_attr from neutron.db import tag_db as tag_methods -from neutron.extensions import l3 as l3_ext -from neutron.extensions import tag as tag_ext +from neutron.extensions import tagging from neutron.objects import exceptions as obj_exc from neutron.objects import tag as tag_obj # Taggable resources -resource_model_map = { - # When we'll add other resources, we must add new extension for them - # if we don't have better discovery mechanism instead of it. - net_def.COLLECTION_NAME: models_v2.Network, - subnet_def.COLLECTION_NAME: models_v2.Subnet, - port_def.COLLECTION_NAME: models_v2.Port, - subnetpool_def.COLLECTION_NAME: models_v2.SubnetPool, - l3_ext.ROUTERS: l3_model.Router, -} +resource_model_map = standard_attr.get_standard_attr_resource_model_map() @resource_extend.has_resource_extenders -class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase): +class TagPlugin(common_db_mixin.CommonDbMixin, tagging.TagPluginBase): """Implementation of the Neutron Tag Service Plugin.""" - supported_extension_aliases = ['tag', 'tag-ext'] + supported_extension_aliases = ['tag', 'tag-ext', 'standard-attr-tag'] def __new__(cls, *args, **kwargs): inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs) @@ -68,7 +54,7 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase): @staticmethod @resource_extend.extends(list(resource_model_map)) def _extend_tags_dict(response_data, db_data): - if not directory.get_plugin(tag_ext.TAG_PLUGIN_TYPE): + if not directory.get_plugin(tagging.TAG_PLUGIN_TYPE): return tags = [tag_db.tag for tag_db in db_data.standard_attr.tags] response_data['tags'] = tags @@ -78,7 +64,7 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase): try: return model_query.get_by_id(context, model, resource_id) except exc.NoResultFound: - raise tag_ext.TagResourceNotFound(resource=resource, + raise tagging.TagResourceNotFound(resource=resource, resource_id=resource_id) @log_helpers.log_method_call @@ -91,7 +77,7 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase): def get_tag(self, context, resource, resource_id, tag): res = self._get_resource(context, resource, resource_id) if not any(tag == tag_db.tag for tag_db in res.standard_attr.tags): - raise tag_ext.TagNotFound(tag=tag) + raise tagging.TagNotFound(tag=tag) @log_helpers.log_method_call @db_api.retry_if_session_inactive() @@ -140,4 +126,4 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tag_ext.TagPluginBase): res = self._get_resource(context, resource, resource_id) if not tag_obj.Tag.delete_objects(context, tag=tag, standard_attr_id=res.standard_attr_id): - raise tag_ext.TagNotFound(tag=tag) + raise tagging.TagNotFound(tag=tag) diff --git a/neutron/services/trunk/models.py b/neutron/services/trunk/models.py index 8dec908549c..5b982ced7fb 100644 --- a/neutron/services/trunk/models.py +++ b/neutron/services/trunk/models.py @@ -45,6 +45,8 @@ class Trunk(standard_attr.HasStandardAttributes, model_base.BASEV2, sub_ports = sa.orm.relationship( 'SubPort', lazy='subquery', uselist=True, cascade="all, delete-orphan") api_collections = ['trunks'] + collection_resource_map = {'trunks': 'trunk'} + tag_support = True class SubPort(model_base.BASEV2): diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index da8dbfd2c0f..d6db6f0a9a2 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -39,6 +39,7 @@ NETWORK_API_EXTENSIONS+=",sorting" NETWORK_API_EXTENSIONS+=",standard-attr-description" 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" diff --git a/neutron/tests/unit/db/test_standard_attr.py b/neutron/tests/unit/db/test_standard_attr.py index 8ac39e854d4..1039ebe4b98 100644 --- a/neutron/tests/unit/db/test_standard_attr.py +++ b/neutron/tests/unit/db/test_standard_attr.py @@ -55,6 +55,40 @@ class StandardAttrTestCase(base.BaseTestCase): with testtools.ExpectedException(RuntimeError): standard_attr.get_standard_attr_resource_model_map() + def test_standard_attr_resource_parent_map(self): + base = self._make_decl_base() + + class TagSupportModel(standard_attr.HasStandardAttributes, + standard_attr.model_base.HasId, + base): + collection_resource_map = {'collection_name': 'member_name'} + tag_support = True + + class TagUnsupportModel(standard_attr.HasStandardAttributes, + standard_attr.model_base.HasId, + base): + collection_resource_map = {'collection_name2': 'member_name2'} + tag_support = False + + class TagUnsupportModel2(standard_attr.HasStandardAttributes, + standard_attr.model_base.HasId, + base): + collection_resource_map = {'collection_name3': 'member_name3'} + + parent_map = standard_attr.get_tag_resource_parent_map() + self.assertEqual('member_name', parent_map['collection_name']) + self.assertNotIn('collection_name2', parent_map) + self.assertNotIn('collection_name3', parent_map) + + class DupTagSupportModel(standard_attr.HasStandardAttributes, + standard_attr.model_base.HasId, + base): + collection_resource_map = {'collection_name': 'member_name'} + tag_support = True + + with testtools.ExpectedException(RuntimeError): + standard_attr.get_tag_resource_parent_map() + class StandardAttrAPIImapctTestCase(testlib_api.SqlTestCase): """Test case to determine if a resource has had new fields exposed.""" @@ -76,6 +110,18 @@ class StandardAttrAPIImapctTestCase(testlib_api.SqlTestCase): set(standard_attr.get_standard_attr_resource_model_map().keys()) ) + def test_api_tag_support_is_expected(self): + # NOTE: If this test is being modified, it means the resources for tag + # support are extended. It changes tag support API. The API change + # should be exposed in release note for API users. And also it should + # be list as other tag support resources in doc/source/devref/tag.rst + expected = ['subnets', 'trunks', 'routers', 'networks', 'policies', + 'subnetpools', 'ports', 'security_groups', 'floatingips'] + self.assertEqual( + set(expected), + set(standard_attr.get_tag_resource_parent_map().keys()) + ) + class StandardAttrRevisesBulkDeleteTestCase(testlib_api.SqlTestCase): diff --git a/neutron/tests/unit/extensions/test_tag.py b/neutron/tests/unit/extensions/test_tag.py index a159accd599..359c38f1638 100644 --- a/neutron/tests/unit/extensions/test_tag.py +++ b/neutron/tests/unit/extensions/test_tag.py @@ -10,40 +10,67 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron_lib import context +from oslo_utils import uuidutils import testscenarios from neutron.api import extensions from neutron.api.v2 import attributes 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.db import test_db_base_plugin_v2 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 TestTagApiBase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, +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(resource='networks', + dict(collection='networks', member='network')), ('Subnet Tag Test', - dict(resource='subnets', + dict(collection='subnets', member='subnet')), ('Port Tag Test', - dict(resource='ports', + dict(collection='ports', member='port')), ('Subnetpool Tag Test', - dict(resource='subnetpools', + dict(collection='subnetpools', member='subnetpool')), ('Router Tag Test', - dict(resource='routers', + 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): @@ -51,65 +78,133 @@ class TestTagApiBase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, 'TAG': "neutron.services.tag.tag_plugin.TagPlugin", 'router': "neutron.tests.unit.extensions.test_l3.TestL3NatServicePlugin"} - super(TestTagApiBase, self).setUp(service_plugins=service_plugins) + 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} + extensions_path, {'router': l3_plugin, 'TAG': plugin, + 'sec': sec_plugin} ) ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP) 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.resource == "networks": + if self._is_object(): + return self._make_object() + + if self.collection == "networks": res = self._make_network(self.fmt, 'net1', True) - elif self.resource == "subnets": + 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.resource == "ports": + elif self.collection == "ports": net = self._make_network(self.fmt, 'net1', True) res = self._make_port(self.fmt, net['network']['id']) - elif self.resource == "subnetpools": + elif self.collection == "subnetpools": res = self._make_subnetpool(self.fmt, ['10.0.0.0/8'], name='my pool', tenant_id="tenant") - elif self.resource == "routers": + 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): - res = self._show(self.resource, self.resource_id) + 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.resource, id=self.resource_id, + 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): body = {'tags': tags} - req = self._req('PUT', self.resource, data=body, id=self.resource_id, + 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.resource, id=self.resource_id, + 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.resource, id=self.resource_id, + 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.resource, id=self.resource_id, + 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: @@ -125,10 +220,14 @@ class TestTagApiBase(test_db_base_plugin_v2.NeutronDbPluginV2TestCase, 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.resource, query_params=params) - return res[self.resource] + 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) @@ -142,6 +241,7 @@ 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): @@ -232,16 +332,18 @@ class TestResourceTagFilter(TestTagApiBase): def _make_tags(self, resource_id, tags): body = {'tags': tags} - req = self._req('PUT', self.resource, data=body, id=resource_id, + 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']) @@ -249,38 +351,43 @@ class TestResourceTagFilter(TestTagApiBase): self._make_tags(self.res4, ['green']) # res5: no tags - def _assertEqualResources(self, expected, res): - actual = [n['id'] for n in res] + 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): - res = self._get_tags_filter_resources(tags=['red']) - self._assertEqualResources([self.res1, self.res2, self.res3], res) + resources = self._get_tags_filter_resources(tags=['red']) + self._assertEqualResources([self.res1, self.res2, self.res3], + resources) def test_filter_tags_multi(self): - res = self._get_tags_filter_resources(tags=['red', 'blue']) - self._assertEqualResources([self.res2, self.res3], res) + resources = self._get_tags_filter_resources(tags=['red', 'blue']) + self._assertEqualResources([self.res2, self.res3], resources) def test_filter_tags_any_single(self): - res = self._get_tags_filter_resources(tags_any=['blue']) - self._assertEqualResources([self.res2, self.res3], res) + resources = self._get_tags_filter_resources(tags_any=['blue']) + self._assertEqualResources([self.res2, self.res3], resources) def test_filter_tags_any_multi(self): - res = self._get_tags_filter_resources(tags_any=['red', 'blue']) - self._assertEqualResources([self.res1, self.res2, self.res3], res) + 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): - res = self._get_tags_filter_resources(not_tags=['red']) - self._assertEqualResources([self.res4, self.res5], res) + resources = self._get_tags_filter_resources(not_tags=['red']) + self._assertEqualResources([self.res4, self.res5], resources) def test_filter_not_tags_multi(self): - res = self._get_tags_filter_resources(not_tags=['red', 'blue']) - self._assertEqualResources([self.res1, self.res4, self.res5], res) + 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): - res = self._get_tags_filter_resources(not_tags_any=['blue']) - self._assertEqualResources([self.res1, self.res4, self.res5], res) + 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): - res = self._get_tags_filter_resources(not_tags_any=['red', 'blue']) - self._assertEqualResources([self.res4, self.res5], res) + resources = self._get_tags_filter_resources(not_tags_any=['red', + 'blue']) + self._assertEqualResources([self.res4, self.res5], resources) diff --git a/neutron/tests/unit/objects/extensions/test_standardattributes.py b/neutron/tests/unit/objects/extensions/test_standardattributes.py index 9716ae802a5..b04a1c4c3bb 100644 --- a/neutron/tests/unit/objects/extensions/test_standardattributes.py +++ b/neutron/tests/unit/objects/extensions/test_standardattributes.py @@ -29,6 +29,8 @@ class FakeDbModelWithStandardAttributes( id = sa.Column(sa.String(36), primary_key=True, nullable=False) item = sa.Column(sa.String(64)) api_collections = [] + collection_resource_map = {} + tag_support = False @obj_base.VersionedObjectRegistry.register_if(False) diff --git a/releasenotes/notes/add-tag-all-standardattr-resources-6f757cb39cc1dcfe.yaml b/releasenotes/notes/add-tag-all-standardattr-resources-6f757cb39cc1dcfe.yaml new file mode 100644 index 00000000000..cad1c33f3a9 --- /dev/null +++ b/releasenotes/notes/add-tag-all-standardattr-resources-6f757cb39cc1dcfe.yaml @@ -0,0 +1,10 @@ +--- +features: + - The resource tag mechanism is refactored so that the tag support + for new resources can be supported easily. + The resources with tag support are network, subnet, port, subnetpool, + trunk, floatingip, policy, security_group, and router. +deprecations: + - Users can use 'tagging' extension instead of the 'tag' extension and + 'tag-ext' extension. Those extensions are now deprecated and will be + removed in the Queens release. \ No newline at end of file