From 6f26f4026b528e778fa653f51baff39f889181be Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 25 Jan 2019 09:04:29 +0000 Subject: [PATCH] Change provider network segmentation ID In the ML2 plugin, allow to update the segmentation ID of a single provider network. A new method in the "SimpleAgentMechanismDriverBase" is added: "provider_network_attribute_updates_supported". This method returns, if implemented in the specific agent, which network attributes can be updated on a live network with ports bound to this network back-end. By default, an empty list is returned. Partial-Bug: #1806052 Change-Id: I2595335d6fbc51562b070f14eaeaadf49cf7c418 --- neutron/db/db_base_plugin_v2.py | 4 + neutron/db/segments_db.py | 15 ++++ neutron/extensions/providernet.py | 16 ---- neutron/plugins/ml2/drivers/mech_agent.py | 11 +++ neutron/plugins/ml2/managers.py | 28 +++++++ neutron/plugins/ml2/plugin.py | 61 +++++++++++++- .../plugins/ml2/drivers/mechanism_logger.py | 4 + .../plugins/ml2/drivers/mechanism_test.py | 10 +++ .../tests/unit/plugins/ml2/test_managers.py | 51 ++++++++++++ neutron/tests/unit/plugins/ml2/test_plugin.py | 79 +++++++++++++++++++ 10 files changed, 261 insertions(+), 18 deletions(-) diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 6911891b8d9..16f6509c44b 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -18,6 +18,7 @@ import functools import netaddr from neutron_lib.api.definitions import ip_allocation as ipalloc_apidef from neutron_lib.api.definitions import port as port_def +from neutron_lib.api.definitions import portbindings as portbindings_def from neutron_lib.api.definitions import subnetpool as subnetpool_def from neutron_lib.api import validators from neutron_lib.callbacks import events @@ -1476,11 +1477,14 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, filters = filters or {} fixed_ips = filters.pop('fixed_ips', {}) + vif_type = filters.pop(portbindings_def.VIF_TYPE, None) query = model_query.get_collection_query(context, Port, filters=filters, *args, **kwargs) ip_addresses = fixed_ips.get('ip_address') subnet_ids = fixed_ips.get('subnet_id') + if vif_type is not None: + query = query.filter(Port.port_bindings.any(vif_type=vif_type)) if ip_addresses: query = query.filter( Port.fixed_ips.any(IPAllocation.ip_address.in_(ip_addresses))) diff --git a/neutron/db/segments_db.py b/neutron/db/segments_db.py index f00bd057996..bd7f8b48913 100644 --- a/neutron/db/segments_db.py +++ b/neutron/db/segments_db.py @@ -15,11 +15,13 @@ from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib.db import api as db_api +from neutron_lib.plugins.ml2 import api as ml2_api from oslo_log import log as logging from oslo_utils import uuidutils from neutron.objects import base as base_obj from neutron.objects import network as network_obj +from neutron.services.segments import exceptions as segments_exceptions LOG = logging.getLogger(__name__) @@ -61,6 +63,19 @@ def add_network_segment(context, network_id, segment, segment_index=0, 'network_id': netseg_obj.network_id}) +def update_network_segment(context, segment_id, segmentation_id): + with db_api.CONTEXT_WRITER.using(context): + netseg_obj = network_obj.NetworkSegment.get_object(context, + id=segment_id) + if not netseg_obj: + raise segments_exceptions.SegmentNotFound(segment_id=segment_id) + netseg_obj[ml2_api.SEGMENTATION_ID] = segmentation_id + netseg_obj.update() + + LOG.info("Updated segment %(id)s, segmentation_id: %(segmentation_id)s)", + {'id': segment_id, 'segmentation_id': segmentation_id}) + + def get_network_segments(context, network_id, filter_dynamic=False): return get_networks_segments( context, [network_id], filter_dynamic)[network_id] diff --git a/neutron/extensions/providernet.py b/neutron/extensions/providernet.py index 5f7091f5189..1ca73c8255f 100644 --- a/neutron/extensions/providernet.py +++ b/neutron/extensions/providernet.py @@ -15,22 +15,6 @@ from neutron_lib.api.definitions import provider_net from neutron_lib.api import extensions -from neutron_lib.api import validators -from neutron_lib import exceptions as n_exc - -from neutron._i18n import _ - - -def _raise_if_updates_provider_attributes(attrs): - """Raise exception if provider attributes are present. - - This method is used for plugins that do not support - updating provider networks. - """ - if any(validators.is_attr_set(attrs.get(a)) - for a in provider_net.ATTRIBUTES): - msg = _("Plugin does not support updating provider attributes") - raise n_exc.InvalidInput(error_message=msg) class Providernet(extensions.APIExtensionDescriptor): diff --git a/neutron/plugins/ml2/drivers/mech_agent.py b/neutron/plugins/ml2/drivers/mech_agent.py index 5b41cdfea79..d806fdfca35 100644 --- a/neutron/plugins/ml2/drivers/mech_agent.py +++ b/neutron/plugins/ml2/drivers/mech_agent.py @@ -363,3 +363,14 @@ class SimpleAgentMechanismDriverBase(AgentMechanismDriverBase): return False return True + + @staticmethod + def provider_network_attribute_updates_supported(): + """Returns the provider network attributes that can be updated + + Possible values: neutron_lib.api.definitions.provider_net.ATTRIBUTES + + :returns: (list) provider network attributes that can be updated in a + live network using this driver. + """ + return [] diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index 21d9e436108..d738323ebcd 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -200,6 +200,10 @@ class TypeManager(stevedore.named.NamedExtensionManager): segments_db.add_network_segment( context, network_id, segment, segment_index) + def _update_network_segment(self, context, network_id, segmentation_id): + segments_db.update_network_segment( + context, network_id, segmentation_id) + def create_network_segments(self, context, network, tenant_id): """Call type drivers to create network segments.""" segments = self._process_provider_create(network) @@ -222,6 +226,30 @@ class TypeManager(stevedore.named.NamedExtensionManager): context, filters=filters) self._add_network_segment(context, network_id, segment) + def update_network_segment(self, context, network, net_data, segment): + """Call type drivers to update a network segment. + + Update operation is currently only supported for VLAN type segments, + and only the SEGMENTATION_ID field can be changed. + """ + segmentation_id = net_data.get(provider.SEGMENTATION_ID) + network_type = segment[api.NETWORK_TYPE] + if network_type != constants.TYPE_VLAN: + msg = (_('Only VLAN type networks can be updated.')) + raise exc.InvalidInput(error_message=msg) + elif not segmentation_id: + msg = (_('Only %s field can be updated in VLAN type networks') % + api.SEGMENTATION_ID) + raise exc.InvalidInput(error_message=msg) + + new_segment = {api.NETWORK_TYPE: segment[api.NETWORK_TYPE], + api.PHYSICAL_NETWORK: segment[api.PHYSICAL_NETWORK], + api.SEGMENTATION_ID: segmentation_id} + self.validate_provider_segment(new_segment) + self.reserve_provider_segment(context, new_segment) + self._update_network_segment(context, segment['id'], segmentation_id) + self.release_network_segment(context, segment) + def reserve_network_segment(self, context, segment_data): """Call type drivers to reserve a network segment.""" # Validate the data of segment diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index a8a570872c4..6ce641a8cdf 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -62,6 +62,7 @@ from neutron_lib.db import utils as db_utils from neutron_lib import exceptions as exc from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.exceptions import port_security as psec_exc +from neutron_lib.objects import utils as obj_utils from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api @@ -107,7 +108,6 @@ from neutron.db import segments_db from neutron.db import subnet_service_type_mixin from neutron.db import vlantransparent_db from neutron.extensions import filter_validation -from neutron.extensions import providernet as provider from neutron.extensions import vlantransparent from neutron.ipam import exceptions as ipam_exc from neutron.objects import base as base_obj @@ -797,6 +797,61 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, segment[api.SEGMENTATION_ID], segment[api.PHYSICAL_NETWORK]) + def _update_segmentation_id(self, context, network, net_data): + """Update segmentation ID in a single provider network""" + segments = segments_db.get_networks_segments( + context, [network['id']])[network['id']] + if len(segments) > 1: + msg = _('Provider network attributes can be updated only in ' + 'provider networks with a single segment.') + raise exc.InvalidInput(error_message=msg) + + vif_types = [portbindings.VIF_TYPE_UNBOUND, + portbindings.VIF_TYPE_BINDING_FAILED] + for mech_driver in self.mechanism_manager.ordered_mech_drivers: + if (provider_net.SEGMENTATION_ID in mech_driver.obj. + provider_network_attribute_updates_supported()): + agent_type = mech_driver.obj.agent_type + agents = self.get_agents(context, + filters={'agent_type': [agent_type]}) + for agent in agents: + vif_types.append(mech_driver.obj.get_vif_type( + context, agent, segments[0])) + + filter_obj = obj_utils.NotIn(vif_types) + filters = {portbindings.VIF_TYPE: + filter_obj.filter(models.PortBinding.vif_type), + 'network_id': [network['id']]} + if super(Ml2Plugin, self).get_ports_count(context, + filters=filters): + msg = (_('Provider network attribute %(attr)s cannot be updated ' + 'if any port in the network has not the following ' + '%(vif_field)s: %(vif_types)s') % + {'attr': provider_net.SEGMENTATION_ID, + 'vif_field': portbindings.VIF_TYPE, + 'vif_types': ', '.join(vif_types)}) + raise exc.InvalidInput(error_message=msg) + + self.type_manager.update_network_segment(context, network, + net_data, segments[0]) + + def _update_provider_network_attributes(self, context, network, net_data): + """Raise exception if provider network attrs update are not supported. + + This function will raise an exception if the provider network attribute + update is not supported. + """ + if net_data.get(provider_net.SEGMENTATION_ID): + self._update_segmentation_id(context, network, net_data) + + provider_net_attrs = (set(provider_net.ATTRIBUTES) - + {provider_net.SEGMENTATION_ID}) + if any(validators.is_attr_set(net_data.get(a)) + for a in provider_net_attrs): + msg = (_('Plugin does not support updating the following provider ' + 'network attributes: %s') % ', '.join(provider_net_attrs)) + raise exc.InvalidInput(error_message=msg) + def _delete_objects(self, context, resource, objects): delete_op = getattr(self, 'delete_%s' % resource) for obj in objects: @@ -985,11 +1040,13 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, @db_api.retry_if_session_inactive() def update_network(self, context, id, network): net_data = network[net_def.RESOURCE_NAME] - provider._raise_if_updates_provider_attributes(net_data) need_network_update_notify = False with db_api.CONTEXT_WRITER.using(context): original_network = super(Ml2Plugin, self).get_network(context, id) + self._update_provider_network_attributes( + context, original_network, net_data) + updated_network = super(Ml2Plugin, self).update_network(context, id, network) diff --git a/neutron/tests/unit/plugins/ml2/drivers/mechanism_logger.py b/neutron/tests/unit/plugins/ml2/drivers/mechanism_logger.py index e7d3ba41fc0..675766c25d4 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/mechanism_logger.py +++ b/neutron/tests/unit/plugins/ml2/drivers/mechanism_logger.py @@ -140,3 +140,7 @@ class LoggerMechanismDriver(api.MechanismDriver): "%(segments)s, candidate hosts %(hosts)s ", {'segments': segments, 'hosts': candidate_hosts}) return set() + + @staticmethod + def provider_network_attribute_updates_supported(): + return [] diff --git a/neutron/tests/unit/plugins/ml2/drivers/mechanism_test.py b/neutron/tests/unit/plugins/ml2/drivers/mechanism_test.py index 3e6ebe2ae0b..c8a5b7837b9 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/mechanism_test.py +++ b/neutron/tests/unit/plugins/ml2/drivers/mechanism_test.py @@ -20,6 +20,9 @@ from neutron_lib import constants as const from neutron_lib.plugins.ml2 import api +VIF_TYPE_TEST = 'vif_type_test' + + class TestMechanismDriver(api.MechanismDriver): """Test mechanism driver for testing mechanism driver api.""" @@ -246,6 +249,9 @@ class TestMechanismDriver(api.MechanismDriver): self, context, segments, candidate_hosts, agent_getter): return set() + def get_vif_type(self, context, agent, segment): + return VIF_TYPE_TEST + @property def resource_provider_uuid5_namespace(self): return uuid.UUID('7f0ce65c-1f13-11e9-8921-3c6aa7b21d17') @@ -260,3 +266,7 @@ class TestMechanismDriver(api.MechanismDriver): def get_standard_device_mappings(self, agent): return {} + + @staticmethod + def provider_network_attribute_updates_supported(): + return [] diff --git a/neutron/tests/unit/plugins/ml2/test_managers.py b/neutron/tests/unit/plugins/ml2/test_managers.py index dfb53080286..f585f16508a 100644 --- a/neutron/tests/unit/plugins/ml2/test_managers.py +++ b/neutron/tests/unit/plugins/ml2/test_managers.py @@ -16,11 +16,15 @@ import mock +from neutron_lib.api.definitions import provider_net as provider +from neutron_lib import exceptions as exc from neutron_lib.exceptions import placement as place_exc from neutron_lib.plugins.ml2 import api from oslo_config import cfg from oslo_db import exception as db_exc +from oslo_utils import uuidutils +from neutron.db import segments_db from neutron.plugins.ml2.common import exceptions as ml2_exc from neutron.plugins.ml2 import managers from neutron.tests import base @@ -180,3 +184,50 @@ class TestMechManager(base.BaseTestCase): def test_port_precommit(self): self._check_resource('port') + + +class TypeManagerTestCase(base.BaseTestCase): + + def setUp(self): + super(TypeManagerTestCase, self).setUp() + self.type_manager = managers.TypeManager() + self.ctx = mock.Mock() + self.network = {'id': uuidutils.generate_uuid()} + + def test_update_network_segment_no_vlan_no_segmentation_id(self): + net_data = {} + segment = {api.NETWORK_TYPE: 'vlan'} + self.assertRaises( + exc.InvalidInput, self.type_manager.update_network_segment, + self.ctx, self.network, net_data, segment) + + net_data = {provider.SEGMENTATION_ID: 1000} + segment = {api.NETWORK_TYPE: 'no_vlan'} + self.assertRaises( + exc.InvalidInput, self.type_manager.update_network_segment, + self.ctx, self.network, net_data, segment) + + def test_update_network_segment(self): + segmentation_id = 1000 + net_data = {provider.SEGMENTATION_ID: segmentation_id} + segment = {'id': uuidutils.generate_uuid(), + api.NETWORK_TYPE: 'vlan', + api.PHYSICAL_NETWORK: 'default_network'} + new_segment = {api.NETWORK_TYPE: 'vlan', + api.PHYSICAL_NETWORK: 'default_network', + api.SEGMENTATION_ID: segmentation_id} + with mock.patch.object(self.type_manager, + 'validate_provider_segment') as mock_validate, \ + mock.patch.object(self.type_manager, + 'reserve_provider_segment') as mock_reserve,\ + mock.patch.object(self.type_manager, + 'release_network_segment') as mock_release, \ + mock.patch.object(segments_db, 'update_network_segment') as \ + mock_update_network_segment: + self.type_manager.update_network_segment(self.ctx, self.network, + net_data, segment) + mock_validate.assert_called_once_with(new_segment) + mock_reserve.assert_called_once_with(self.ctx, new_segment) + mock_update_network_segment.assert_called_once_with( + self.ctx, segment['id'], segmentation_id) + mock_release.assert_called_once_with(self.ctx, segment) diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index 55175fa7797..c27b6e33753 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -36,6 +36,7 @@ from neutron_lib import context from neutron_lib.db import api as db_api from neutron_lib import exceptions as exc from neutron_lib import fixture +from neutron_lib.objects import utils as obj_utils from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api as driver_api @@ -49,6 +50,7 @@ import webob from neutron._i18n import _ from neutron.common import utils from neutron.db import agents_db +from neutron.db import db_base_plugin_v2 from neutron.db import provisioning_blocks from neutron.db import securitygroups_db as sg_db from neutron.db import segments_db @@ -419,6 +421,83 @@ class TestMl2NetworksV2(test_plugin.TestNetworksV2, self.assertEqual(3, f.call_count) retry_fixture.cleanUp() + def test__update_provider_network_attributes_no_segmentation_id(self): + plugin = directory.get_plugin() + with self.network() as net: + for attribute in set(pnet.ATTRIBUTES) - {pnet.SEGMENTATION_ID}: + net_data = {attribute: 'attribute_value'} + self.assertRaises( + exc.InvalidInput, + plugin._update_provider_network_attributes, + self.context, net['network'], net_data) + + def test__update_provider_network_attributes_segmentation_id(self): + plugin = directory.get_plugin() + with self.network() as net: + with mock.patch.object(plugin, '_update_segmentation_id') as \ + mock_update_segmentation_id: + net_data = {pnet.SEGMENTATION_ID: 1000} + plugin._update_provider_network_attributes( + self.context, net['network'], net_data) + mock_update_segmentation_id.assert_called_once_with( + self.context, net['network'], net_data) + + def test__update_segmentation_id_multisegment_network(self): + plugin = directory.get_plugin() + segments = [{pnet.NETWORK_TYPE: 'vlan', + pnet.PHYSICAL_NETWORK: 'physnet1', + pnet.SEGMENTATION_ID: 1}, + {pnet.NETWORK_TYPE: 'vlan', + pnet.PHYSICAL_NETWORK: 'physnet1', + pnet.SEGMENTATION_ID: 2}] + with self.network(**{'arg_list': (mpnet_apidef.SEGMENTS, ), + mpnet_apidef.SEGMENTS: segments}) as net: + self.assertRaises( + exc.InvalidInput, plugin._update_segmentation_id, self.context, + net['network'], {}) + + def test__update_segmentation_id_ports_wrong_vif_type(self): + plugin = directory.get_plugin() + with self.network() as net: + with mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + 'get_ports_count') as mock_get_ports_count: + mock_get_ports_count.return_value = 1 + self.assertRaises( + exc.InvalidInput, plugin._update_segmentation_id, + self.context, net['network'], {}) + + def test__update_segmentation_id_ports(self): + plugin = directory.get_plugin() + segments = [{pnet.NETWORK_TYPE: 'vlan', + pnet.PHYSICAL_NETWORK: 'physnet1', + pnet.SEGMENTATION_ID: 1}] + with self.network(**{'arg_list': (mpnet_apidef.SEGMENTS, ), + mpnet_apidef.SEGMENTS: segments}) as net, \ + mock.patch.object(db_base_plugin_v2.NeutronDbPluginV2, + 'get_ports_count') as mock_get_ports_count, \ + mock.patch.object(plugin.type_manager, + 'update_network_segment'), \ + mock.patch.object( + mech_test.TestMechanismDriver, + 'provider_network_attribute_updates_supported', + return_value=[pnet.SEGMENTATION_ID]), \ + mock.patch.object(plugin, 'get_agents', + return_value=[mock.ANY]), \ + mock.patch.object(obj_utils, 'NotIn') as mock_not_in: + mock_get_ports_count.return_value = 0 + net_data = {pnet.SEGMENTATION_ID: 1000} + plugin._update_segmentation_id(self.context, net['network'], + net_data) + + mock_not_in.assert_called_once_with([ + portbindings.VIF_TYPE_UNBOUND, + portbindings.VIF_TYPE_BINDING_FAILED, + mech_test.VIF_TYPE_TEST]) + filters = {portbindings.VIF_TYPE: mock.ANY, + 'network_id': [net['network']['id']]} + mock_get_ports_count.assert_called_once_with( + self.context, filters=filters) + class TestExternalNetwork(Ml2PluginV2TestCase):