Merge "Tag mechanism supports resources with standard attribute"
This commit is contained in:
commit
8718d5c2b6
@ -88,6 +88,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
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
258
neutron/extensions/tagging.py
Normal file
258
neutron/extensions/tagging.py
Normal 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
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -40,6 +40,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"
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user