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
This commit is contained in:
Hirofumi Ichihara 2017-06-16 16:46:37 +09:00
parent a51271d760
commit 96f0142b80
21 changed files with 596 additions and 234 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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