diff --git a/neutron/extensions/tag_ports_during_bulk_creation.py b/neutron/extensions/tag_ports_during_bulk_creation.py new file mode 100644 index 00000000000..f7d37af7364 --- /dev/null +++ b/neutron/extensions/tag_ports_during_bulk_creation.py @@ -0,0 +1,24 @@ +# Copyright (c) 2019 Verizon Media +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.api.definitions import \ + tag_ports_during_bulk_creation as apidef +from neutron_lib.api import extensions + + +class Tag_ports_during_bulk_creation(extensions.APIExtensionDescriptor): + """Extension to tag ports during bulk creation.""" + + api_definition = apidef diff --git a/neutron/extensions/tagging.py b/neutron/extensions/tagging.py index b65978ee97c..21e690c6e49 100644 --- a/neutron/extensions/tagging.py +++ b/neutron/extensions/tagging.py @@ -12,7 +12,9 @@ # under the License. import abc +import copy +from neutron_lib.api.definitions import port from neutron_lib.api import extensions as api_extensions from neutron_lib.api import faults from neutron_lib.api import validators @@ -48,6 +50,12 @@ TAG_ATTRIBUTE_MAP = { NOT_TAGS_ANY: {'allow_post': False, 'allow_put': False, 'is_visible': False, 'is_filter': True}, } +TAG_ATTRIBUTE_MAP_PORTS = copy.deepcopy(TAG_ATTRIBUTE_MAP) +TAG_ATTRIBUTE_MAP_PORTS[TAGS] = { + 'allow_post': True, 'allow_put': False, + 'validate': {'type:list_of_unique_strings': MAX_TAG_LEN}, + 'default': [], 'is_visible': True, 'is_filter': True +} class TagResourceNotFound(exceptions.NotFound): @@ -210,7 +218,11 @@ class Tagging(api_extensions.ExtensionDescriptor): return {} EXTENDED_ATTRIBUTES_2_0 = {} for collection_name in TAG_SUPPORTED_RESOURCES: - EXTENDED_ATTRIBUTES_2_0[collection_name] = TAG_ATTRIBUTE_MAP + if collection_name == port.COLLECTION_NAME: + EXTENDED_ATTRIBUTES_2_0[collection_name] = ( + TAG_ATTRIBUTE_MAP_PORTS) + else: + EXTENDED_ATTRIBUTES_2_0[collection_name] = TAG_ATTRIBUTE_MAP return EXTENDED_ATTRIBUTES_2_0 diff --git a/neutron/plugins/ml2/extensions/tag_ports_during_bulk_creation.py b/neutron/plugins/ml2/extensions/tag_ports_during_bulk_creation.py new file mode 100644 index 00000000000..48842ba4c21 --- /dev/null +++ b/neutron/plugins/ml2/extensions/tag_ports_during_bulk_creation.py @@ -0,0 +1,58 @@ +# Copyright (c) 2019 Verizon Media +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.api.definitions import \ + tag_ports_during_bulk_creation as apidef +from neutron_lib.plugins import directory +from neutron_lib.plugins.ml2 import api +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron.extensions import tagging + +LOG = logging.getLogger(__name__) + + +class TagPortsDuringBulkCreationExtensionDriver(api.ExtensionDriver): + _supported_extension_alias = apidef.ALIAS + + def initialize(self): + LOG.info("TagPortsDuringBulkCreationExtensionDriver " + "initialization complete") + + @property + def extension_alias(self): + return self._supported_extension_alias + + @property + def tag_plugin(self): + if not hasattr(self, '_tag_plugin'): + self._tag_plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE) + return self._tag_plugin + + @property + def plugin(self): + if not hasattr(self, '_plugin'): + self._plugin = directory.get_plugin() + return self._plugin + + @log_helpers.log_method_call + def process_create_port(self, plugin_context, request_data, db_data): + tags = request_data.get('tags') + if not (self.tag_plugin and tags): + return + port_db = self.plugin._get_port(plugin_context, db_data['id']) + self.tag_plugin.add_tags(plugin_context, port_db.standard_attr_id, + tags) diff --git a/neutron/services/tag/tag_plugin.py b/neutron/services/tag/tag_plugin.py index ccc9aab9fdf..b38d2edf57a 100644 --- a/neutron/services/tag/tag_plugin.py +++ b/neutron/services/tag/tag_plugin.py @@ -90,11 +90,14 @@ class TagPlugin(tagging.TagPluginBase): if tag_db.tag in tags_removed ] ) - for tag in tags_added: - tag_obj.Tag(context, standard_attr_id=res.standard_attr_id, - tag=tag).create() + self.add_tags(context, res.standard_attr_id, tags_added) return body + def add_tags(self, context, standard_attr_id, tags): + for tag in tags: + tag_obj.Tag(context, standard_attr_id=standard_attr_id, + tag=tag).create() + @log_helpers.log_method_call def update_tag(self, context, resource, resource_id, tag): res = self._get_resource(context, resource, resource_id) diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 2db594226b2..0c7bd9b6a8c 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -63,6 +63,7 @@ NETWORK_API_EXTENSIONS+=",standard-attr-timestamp" NETWORK_API_EXTENSIONS+=",standard-attr-tag" NETWORK_API_EXTENSIONS+=",subnet_allocation" NETWORK_API_EXTENSIONS+=",subnet-dns-publish-fixed-ip" +NETWORK_API_EXTENSIONS+=",tag-ports-during-bulk-creation" NETWORK_API_EXTENSIONS+=",trunk" NETWORK_API_EXTENSIONS+=",trunk-details" NETWORK_API_EXTENSIONS+=",uplink-status-propagation" diff --git a/neutron/tests/unit/plugins/ml2/extensions/test_tag_ports_during_bulk_creation.py b/neutron/tests/unit/plugins/ml2/extensions/test_tag_ports_during_bulk_creation.py new file mode 100644 index 00000000000..499fb08d8b3 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/extensions/test_tag_ports_during_bulk_creation.py @@ -0,0 +1,129 @@ +# Copyright (c) 2019 Verizon Media +# All Rights Reserved. +# +# 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 copy + +import mock +from neutron_lib.plugins import directory +from oslo_config import cfg + +from neutron.plugins.ml2.extensions import tag_ports_during_bulk_creation +from neutron.tests.unit.plugins.ml2 import test_plugin + + +TAGS = [ + ['tag-1', 'tag-2', 'tag-3'], + ['tag-1', 'tag-2'], + ['tag-1', 'tag-3'], + [] +] + + +class TagPortsDuringBulkCreationTestCase(test_plugin.Ml2PluginV2TestCase): + _extension_drivers = ['tag_ports_during_bulk_creation'] + fmt = 'json' + + def get_additional_service_plugins(self): + p = super(TagPortsDuringBulkCreationTestCase, + self).get_additional_service_plugins() + p.update({'tag_name': 'tag'}) + return p + + def setUp(self): + cfg.CONF.set_override('extension_drivers', + self._extension_drivers, + group='ml2') + super(TagPortsDuringBulkCreationTestCase, self).setUp() + self.plugin = directory.get_plugin() + + def test_create_ports_bulk_with_tags(self): + num_ports = 3 + tenant_id = 'some_tenant' + with self.network(tenant_id=tenant_id) as network_to_use: + net_id = network_to_use['network']['id'] + port = {'port': {'network_id': net_id, + 'admin_state_up': True, + 'tenant_id': tenant_id}} + ports = [copy.deepcopy(port) for x in range(num_ports)] + ports_tags_map = {} + for port, tags in zip(ports, TAGS): + port['port']['tags'] = tags + port['port']['name'] = '-'.join(tags) + ports_tags_map[port['port']['name']] = tags + req_body = {'ports': ports} + ports_req = self.new_create_request('ports', req_body) + res = ports_req.get_response(self.api) + self.assertEqual(201, res.status_int) + created_ports = self.deserialize(self.fmt, res) + + for port in created_ports['ports']: + self.assertEqual(ports_tags_map[port['name']], port['tags']) + + def test_create_ports_bulk_no_tags(self): + num_ports = 2 + tenant_id = 'some_tenant' + with self.network(tenant_id=tenant_id) as network_to_use: + net_id = network_to_use['network']['id'] + port = {'port': {'name': 'port', + 'network_id': net_id, + 'admin_state_up': True, + 'tenant_id': tenant_id}} + ports = [copy.deepcopy(port) for x in range(num_ports)] + req_body = {'ports': ports} + ports_req = self.new_create_request('ports', req_body) + res = ports_req.get_response(self.api) + self.assertEqual(201, res.status_int) + created_ports = self.deserialize(self.fmt, res) + for port in created_ports['ports']: + self.assertFalse(port['tags']) + + def test_create_port_with_tags(self): + tenant_id = 'some_tenant' + with self.network(tenant_id=tenant_id) as network_to_use: + net_id = network_to_use['network']['id'] + req_body = {'port': {'name': 'port', + 'network_id': net_id, + 'admin_state_up': True, + 'tenant_id': tenant_id, + 'tags': TAGS[0]}} + port_req = self.new_create_request('ports', req_body) + res = port_req.get_response(self.api) + self.assertEqual(201, res.status_int) + created_port = self.deserialize(self.fmt, res) + self.assertEqual(TAGS[0], created_port['port']['tags']) + + def test_type_args_passed_to_extension(self): + num_ports = 2 + tenant_id = 'some_tenant' + extension = tag_ports_during_bulk_creation + with mock.patch.object( + extension.TagPortsDuringBulkCreationExtensionDriver, + 'process_create_port') as patched_method: + with self.network(tenant_id=tenant_id) as network_to_use: + net_id = network_to_use['network']['id'] + port = {'port': {'network_id': net_id, + 'admin_state_up': True, + 'tenant_id': tenant_id}} + ports = [copy.deepcopy(port) for x in range(num_ports)] + ports[0]['port']['tags'] = TAGS[0] + ports[1]['port']['tags'] = TAGS[1] + req_body = {'ports': ports} + ports_req = self.new_create_request('ports', req_body) + res = ports_req.get_response(self.api) + self.assertEqual(201, res.status_int) + self.assertIsInstance(patched_method.call_args.args[1], + dict) + self.assertIsInstance(patched_method.call_args.args[2], + dict) diff --git a/releasenotes/notes/tag-ports-during-bulk-creation-23161dd39d779e99.yaml b/releasenotes/notes/tag-ports-during-bulk-creation-23161dd39d779e99.yaml new file mode 100644 index 00000000000..9fbcd328c49 --- /dev/null +++ b/releasenotes/notes/tag-ports-during-bulk-creation-23161dd39d779e99.yaml @@ -0,0 +1,5 @@ +--- +features: + - The ``tag_ports_during_bulk_creation`` ML2 plugin extension has been + implemented to support tagging ports during bulk creation. As a side + effect, this extension also allows tagging ports during non-bulk creation. diff --git a/setup.cfg b/setup.cfg index d8d25b30c7a..ae2a7e5fdb8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -107,6 +107,7 @@ neutron.ml2.extension_drivers = data_plane_status = neutron.plugins.ml2.extensions.data_plane_status:DataPlaneStatusExtensionDriver dns_domain_ports = neutron.plugins.ml2.extensions.dns_integration:DNSDomainPortsExtensionDriver uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver + tag_ports_during_bulk_creation = neutron.plugins.ml2.extensions.tag_ports_during_bulk_creation:TagPortsDuringBulkCreationExtensionDriver subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver neutron.ipam_drivers = fake = neutron.tests.unit.ipam.fake_driver:FakeDriver