From 6c38103e095dbf107bdb199979be8e5e909d7e74 Mon Sep 17 00:00:00 2001 From: Arvind Somya Date: Mon, 18 Aug 2014 08:59:44 -0700 Subject: [PATCH] ML2 Type Driver refactor part 2 This commit builds on top of part 1 to introduce support for creating dynamic network segments in ML2. Change-Id: I399e1569baae6f24054aac15c4c51a2e44a20e5b Partially implements: Blueprint ml2-type-driver-refactor --- ...f57ab_ml2_refactor_for_dynamic_segments.py | 41 +++++ .../alembic_migrations/versions/HEAD | 2 +- neutron/plugins/ml2/db.py | 73 +++++++-- neutron/plugins/ml2/driver_api.py | 25 +++ neutron/plugins/ml2/driver_context.py | 10 ++ neutron/plugins/ml2/managers.py | 55 +++++-- neutron/plugins/ml2/models.py | 2 + neutron/plugins/ml2/plugin.py | 4 +- neutron/tests/unit/ml2/_test_mech_agent.py | 6 + neutron/tests/unit/ml2/test_ml2_plugin.py | 151 +++++++++++++++++- 10 files changed, 335 insertions(+), 34 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/236b90af57ab_ml2_refactor_for_dynamic_segments.py diff --git a/neutron/db/migration/alembic_migrations/versions/236b90af57ab_ml2_refactor_for_dynamic_segments.py b/neutron/db/migration/alembic_migrations/versions/236b90af57ab_ml2_refactor_for_dynamic_segments.py new file mode 100644 index 000000000..4a4db3d80 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/236b90af57ab_ml2_refactor_for_dynamic_segments.py @@ -0,0 +1,41 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""ml2_type_driver_refactor_dynamic_segments + +Revision ID: 236b90af57ab +Revises: 58fe87a01143 +Create Date: 2014-08-14 16:22:14.293788 + +""" + +# revision identifiers, used by Alembic. +revision = '236b90af57ab' +down_revision = '58fe87a01143' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(active_plugins=None, options=None): + + op.add_column('ml2_network_segments', + sa.Column('is_dynamic', sa.Boolean(), nullable=False, + server_default=sa.sql.false())) + + +def downgrade(active_plugins=None, options=None): + + op.drop_column('ml2_network_segments', 'is_dynamic') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 5c763b0d2..dbff3d53e 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -58fe87a01143 +236b90af57ab diff --git a/neutron/plugins/ml2/db.py b/neutron/plugins/ml2/db.py index 242b19ae9..d8caa9384 100644 --- a/neutron/plugins/ml2/db.py +++ b/neutron/plugins/ml2/db.py @@ -31,16 +31,26 @@ from neutron.plugins.ml2 import models LOG = log.getLogger(__name__) -def add_network_segment(session, network_id, segment): +def _make_segment_dict(record): + """Make a segment dictionary out of a DB record.""" + return {api.ID: record.id, + api.NETWORK_TYPE: record.network_type, + api.PHYSICAL_NETWORK: record.physical_network, + api.SEGMENTATION_ID: record.segmentation_id} + + +def add_network_segment(session, network_id, segment, is_dynamic=False): with session.begin(subtransactions=True): record = models.NetworkSegment( id=uuidutils.generate_uuid(), network_id=network_id, network_type=segment.get(api.NETWORK_TYPE), physical_network=segment.get(api.PHYSICAL_NETWORK), - segmentation_id=segment.get(api.SEGMENTATION_ID) + segmentation_id=segment.get(api.SEGMENTATION_ID), + is_dynamic=is_dynamic ) session.add(record) + segment[api.ID] = record.id LOG.info(_("Added segment %(id)s of type %(network_type)s for network" " %(network_id)s"), {'id': record.id, @@ -48,15 +58,58 @@ def add_network_segment(session, network_id, segment): 'network_id': record.network_id}) -def get_network_segments(session, network_id): +def get_network_segments(session, network_id, filter_dynamic=False): + with session.begin(subtransactions=True): + query = (session.query(models.NetworkSegment). + filter_by(network_id=network_id)) + if filter_dynamic is not None: + query = query.filter_by(is_dynamic=filter_dynamic) + records = query.all() + + return [_make_segment_dict(record) for record in records] + + +def get_segment_by_id(session, segment_id): + with session.begin(subtransactions=True): + try: + record = (session.query(models.NetworkSegment). + filter_by(id=segment_id). + one()) + return _make_segment_dict(record) + except exc.NoResultFound: + return + + +def get_dynamic_segment(session, network_id, physical_network=None, + segmentation_id=None): + """Return a dynamic segment for the filters provided if one exists.""" + with session.begin(subtransactions=True): + query = (session.query(models.NetworkSegment). + filter_by(network_id=network_id, is_dynamic=True)) + if physical_network: + query = query.filter_by(physical_network=physical_network) + if segmentation_id: + query = query.filter_by(segmentation_id=segmentation_id) + record = query.first() + + if record: + return _make_segment_dict(record) + else: + LOG.debug("No dynamic segment %s found for " + "Network:%(network_id)s, " + "Physical network:%(physnet)s, " + "segmentation_id:%(segmentation_id)s", + {'network_id': network_id, + 'physnet': physical_network, + 'segmentation_id': segmentation_id}) + return None + + +def delete_network_segment(session, segment_id): + """Release a dynamic segment for the params provided if one exists.""" with session.begin(subtransactions=True): - records = (session.query(models.NetworkSegment). - filter_by(network_id=network_id)) - return [{api.ID: record.id, - api.NETWORK_TYPE: record.network_type, - api.PHYSICAL_NETWORK: record.physical_network, - api.SEGMENTATION_ID: record.segmentation_id} - for record in records] + (session.query(models.NetworkSegment). + filter_by(id=segment_id).delete()) def add_port_binding(session, port_id): diff --git a/neutron/plugins/ml2/driver_api.py b/neutron/plugins/ml2/driver_api.py index 1fe5f8381..cc68181c8 100644 --- a/neutron/plugins/ml2/driver_api.py +++ b/neutron/plugins/ml2/driver_api.py @@ -328,6 +328,31 @@ class PortContext(object): """ pass + @abc.abstractmethod + def allocate_dynamic_segment(self, segment): + """Allocate a dynamic segment. + + :param segment: A partially or fully specified segment dictionary + + Called by the MechanismDriver.bind_port, create_port or update_port + to dynamically allocate a segment for the port using the partial + segment specified. The segment dictionary can be a fully or partially + specified segment. At a minumim it needs the network_type populated to + call on the appropriate type driver. + """ + pass + + @abc.abstractmethod + def release_dynamic_segment(self, segment_id): + """Release an allocated dynamic segment. + + :param segment_id: UUID of the dynamic network segment. + + Called by the MechanismDriver.delete_port or update_port to release + the dynamic segment allocated for this port. + """ + pass + @six.add_metaclass(abc.ABCMeta) class MechanismDriver(object): diff --git a/neutron/plugins/ml2/driver_context.py b/neutron/plugins/ml2/driver_context.py index 2cbc9b561..97987a002 100644 --- a/neutron/plugins/ml2/driver_context.py +++ b/neutron/plugins/ml2/driver_context.py @@ -151,6 +151,16 @@ class PortContext(MechanismDriverContext, api.PortContext): self._binding.vif_details = jsonutils.dumps(vif_details) self._new_port_status = status + def allocate_dynamic_segment(self, segment): + network_id = self._network_context.current['id'] + + return self._plugin.type_manager.allocate_dynamic_segment( + self._plugin_context.session, network_id, segment) + + def release_dynamic_segment(self, segment_id): + return self._plugin.type_manager.release_dynamic_segment( + self._plugin_context.session, segment_id) + class DvrPortContext(PortContext): diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index 084829e58..9c6c6fc51 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -191,20 +191,47 @@ class TypeManager(stevedore.named.NamedExtensionManager): return segment raise exc.NoNetworkAvailable() - def release_segment(self, session, segment): - network_type = segment.get(api.NETWORK_TYPE) - driver = self.drivers.get(network_type) - # ML2 may have been reconfigured since the segment was created, - # so a driver may no longer exist for this network_type. - # REVISIT: network_type-specific db entries may become orphaned - # if a network is deleted and the driver isn't available to release - # the segment. This may be fixed with explicit foreign-key references - # or consistency checks on driver initialization. - if not driver: - LOG.error(_("Failed to release segment '%s' because " - "network type is not supported."), segment) - return - driver.obj.release_segment(session, segment) + def release_network_segments(self, session, network_id): + segments = db.get_network_segments(session, network_id, + filter_dynamic=None) + + for segment in segments: + network_type = segment.get(api.NETWORK_TYPE) + driver = self.drivers.get(network_type) + if driver: + driver.obj.release_segment(session, segment) + else: + LOG.error(_("Failed to release segment '%s' because " + "network type is not supported."), segment) + + def allocate_dynamic_segment(self, session, network_id, segment): + """Allocate a dynamic segment using a partial or full segment dict.""" + dynamic_segment = db.get_dynamic_segment( + session, network_id, segment.get(api.PHYSICAL_NETWORK), + segment.get(api.SEGMENTATION_ID)) + + if dynamic_segment: + return dynamic_segment + + driver = self.drivers.get(segment.get(api.NETWORK_TYPE)) + dynamic_segment = driver.obj.reserve_provider_segment(session, segment) + db.add_network_segment(session, network_id, dynamic_segment, + is_dynamic=True) + return dynamic_segment + + def release_dynamic_segment(self, session, segment_id): + """Delete a dynamic segment.""" + segment = db.get_segment_by_id(session, segment_id) + if segment: + driver = self.drivers.get(segment.get(api.NETWORK_TYPE)) + if driver: + driver.obj.release_segment(session, segment) + db.delete_network_segment(session, segment_id) + else: + LOG.error(_("Failed to release segment '%s' because " + "network type is not supported."), segment) + else: + LOG.debug("No segment found with id %(segment_id)s", segment_id) class MechanismManager(stevedore.named.NamedExtensionManager): diff --git a/neutron/plugins/ml2/models.py b/neutron/plugins/ml2/models.py index 990301723..80ae1bbb6 100644 --- a/neutron/plugins/ml2/models.py +++ b/neutron/plugins/ml2/models.py @@ -39,6 +39,8 @@ class NetworkSegment(model_base.BASEV2, models_v2.HasId): network_type = sa.Column(sa.String(32), nullable=False) physical_network = sa.Column(sa.String(64)) segmentation_id = sa.Column(sa.Integer) + is_dynamic = sa.Column(sa.Boolean, default=False, nullable=False, + server_default=sa.sql.false()) class PortBinding(model_base.BASEV2): diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index f25c51241..85038b682 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -615,9 +615,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, record = self._get_network(context, id) LOG.debug(_("Deleting network record %s"), record) session.delete(record) - - for segment in mech_context.network_segments: - self.type_manager.release_segment(session, segment) + self.type_manager.release_network_segments(session, id) # The segment records are deleted via cascade from the # network record, so explicit removal is not necessary. diff --git a/neutron/tests/unit/ml2/_test_mech_agent.py b/neutron/tests/unit/ml2/_test_mech_agent.py index 89f5394f3..a42eca0c2 100644 --- a/neutron/tests/unit/ml2/_test_mech_agent.py +++ b/neutron/tests/unit/ml2/_test_mech_agent.py @@ -109,6 +109,12 @@ class FakePortContext(api.PortContext): self._bound_vif_type = vif_type self._bound_vif_details = vif_details + def allocate_dynamic_segment(self, segment): + pass + + def release_dynamic_segment(self, segment_id): + pass + class AgentMechanismBaseTestCase(base.BaseTestCase): # The following must be overridden for the specific mechanism diff --git a/neutron/tests/unit/ml2/test_ml2_plugin.py b/neutron/tests/unit/ml2/test_ml2_plugin.py index b01aab0b9..8b61ce87d 100644 --- a/neutron/tests/unit/ml2/test_ml2_plugin.py +++ b/neutron/tests/unit/ml2/test_ml2_plugin.py @@ -69,8 +69,12 @@ class Ml2PluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): group='ml2') self.physnet = 'physnet1' self.vlan_range = '1:100' + self.vlan_range2 = '200:300' + self.physnet2 = 'physnet2' self.phys_vrange = ':'.join([self.physnet, self.vlan_range]) - config.cfg.CONF.set_override('network_vlan_ranges', [self.phys_vrange], + self.phys2_vrange = ':'.join([self.physnet2, self.vlan_range2]) + config.cfg.CONF.set_override('network_vlan_ranges', + [self.phys_vrange, self.phys2_vrange], group='ml2_type_vlan') super(Ml2PluginV2TestCase, self).setUp(PLUGIN_NAME, service_plugins=service_plugins) @@ -368,6 +372,95 @@ class TestMultiSegmentNetworks(Ml2PluginV2TestCase): def setUp(self, plugin=None): super(TestMultiSegmentNetworks, self).setUp() + def test_allocate_dynamic_segment(self): + data = {'network': {'name': 'net1', + 'tenant_id': 'tenant_one'}} + network_req = self.new_create_request('networks', data) + network = self.deserialize(self.fmt, + network_req.get_response(self.api)) + segment = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.PHYSICAL_NETWORK: 'physnet1'} + network_id = network['network']['id'] + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment) + dynamic_segment = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet1') + self.assertEqual('vlan', dynamic_segment[driver_api.NETWORK_TYPE]) + self.assertEqual('physnet1', + dynamic_segment[driver_api.PHYSICAL_NETWORK]) + self.assertTrue(dynamic_segment[driver_api.SEGMENTATION_ID] > 0) + segment2 = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.SEGMENTATION_ID: 1234, + driver_api.PHYSICAL_NETWORK: 'physnet3'} + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment2) + dynamic_segment = ml2_db.get_dynamic_segment(self.context.session, + network_id, + segmentation_id='1234') + self.assertEqual('vlan', dynamic_segment[driver_api.NETWORK_TYPE]) + self.assertEqual('physnet3', + dynamic_segment[driver_api.PHYSICAL_NETWORK]) + self.assertEqual(dynamic_segment[driver_api.SEGMENTATION_ID], 1234) + + def test_allocate_dynamic_segment_multiple_physnets(self): + data = {'network': {'name': 'net1', + 'tenant_id': 'tenant_one'}} + network_req = self.new_create_request('networks', data) + network = self.deserialize(self.fmt, + network_req.get_response(self.api)) + segment = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.PHYSICAL_NETWORK: 'physnet1'} + network_id = network['network']['id'] + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment) + dynamic_segment = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet1') + self.assertEqual('vlan', dynamic_segment[driver_api.NETWORK_TYPE]) + self.assertEqual('physnet1', + dynamic_segment[driver_api.PHYSICAL_NETWORK]) + dynamic_segmentation_id = dynamic_segment[driver_api.SEGMENTATION_ID] + self.assertTrue(dynamic_segmentation_id > 0) + dynamic_segment1 = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet1') + dynamic_segment1_id = dynamic_segment1[driver_api.SEGMENTATION_ID] + self.assertEqual(dynamic_segmentation_id, dynamic_segment1_id) + segment2 = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.PHYSICAL_NETWORK: 'physnet2'} + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment2) + dynamic_segment2 = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet2') + dynamic_segmentation2_id = dynamic_segment2[driver_api.SEGMENTATION_ID] + self.assertNotEqual(dynamic_segmentation_id, dynamic_segmentation2_id) + + def test_allocate_release_dynamic_segment(self): + data = {'network': {'name': 'net1', + 'tenant_id': 'tenant_one'}} + network_req = self.new_create_request('networks', data) + network = self.deserialize(self.fmt, + network_req.get_response(self.api)) + segment = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.PHYSICAL_NETWORK: 'physnet1'} + network_id = network['network']['id'] + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment) + dynamic_segment = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet1') + self.assertEqual('vlan', dynamic_segment[driver_api.NETWORK_TYPE]) + self.assertEqual('physnet1', + dynamic_segment[driver_api.PHYSICAL_NETWORK]) + dynamic_segmentation_id = dynamic_segment[driver_api.SEGMENTATION_ID] + self.assertTrue(dynamic_segmentation_id > 0) + self.driver.type_manager.release_dynamic_segment( + self.context.session, dynamic_segment[driver_api.ID]) + self.assertIsNone(ml2_db.get_dynamic_segment( + self.context.session, network_id, 'physnet1')) + def test_create_network_provider(self): data = {'network': {'name': 'net1', pnet.NETWORK_TYPE: 'vlan', @@ -473,16 +566,62 @@ class TestMultiSegmentNetworks(Ml2PluginV2TestCase): res = network_req.get_response(self.api) self.assertEqual(201, res.status_int) + def test_release_network_segments(self): + data = {'network': {'name': 'net1', + 'admin_state_up': True, + 'shared': False, + pnet.NETWORK_TYPE: 'vlan', + pnet.PHYSICAL_NETWORK: 'physnet1', + pnet.SEGMENTATION_ID: 1, + 'tenant_id': 'tenant_one'}} + network_req = self.new_create_request('networks', data) + res = network_req.get_response(self.api) + network = self.deserialize(self.fmt, res) + network_id = network['network']['id'] + segment = {driver_api.NETWORK_TYPE: 'vlan', + driver_api.PHYSICAL_NETWORK: 'physnet2'} + self.driver.type_manager.allocate_dynamic_segment( + self.context.session, network_id, segment) + dynamic_segment = ml2_db.get_dynamic_segment(self.context.session, + network_id, + 'physnet2') + self.assertEqual('vlan', dynamic_segment[driver_api.NETWORK_TYPE]) + self.assertEqual('physnet2', + dynamic_segment[driver_api.PHYSICAL_NETWORK]) + self.assertTrue(dynamic_segment[driver_api.SEGMENTATION_ID] > 0) + + req = self.new_delete_request('networks', network_id) + res = req.get_response(self.api) + self.assertEqual(ml2_db.get_network_segments( + self.context.session, network_id), []) + self.assertIsNone(ml2_db.get_dynamic_segment( + self.context.session, network_id, 'physnet2')) + def test_release_segment_no_type_driver(self): + data = {'network': {'name': 'net1', + 'admin_state_up': True, + 'shared': False, + pnet.NETWORK_TYPE: 'vlan', + pnet.PHYSICAL_NETWORK: 'physnet1', + pnet.SEGMENTATION_ID: 1, + 'tenant_id': 'tenant_one'}} + network_req = self.new_create_request('networks', data) + res = network_req.get_response(self.api) + network = self.deserialize(self.fmt, res) + network_id = network['network']['id'] + segment = {driver_api.NETWORK_TYPE: 'faketype', driver_api.PHYSICAL_NETWORK: 'physnet1', driver_api.ID: 1} with mock.patch('neutron.plugins.ml2.managers.LOG') as log: - self.driver.type_manager.release_segment(session=None, - segment=segment) - log.error.assert_called_once_with( - "Failed to release segment '%s' because " - "network type is not supported.", segment) + with mock.patch('neutron.plugins.ml2.managers.db') as db: + db.get_network_segments.return_value = (segment,) + self.driver.type_manager.release_network_segments( + self.context.session, network_id) + + log.error.assert_called_once_with( + "Failed to release segment '%s' because " + "network type is not supported.", segment) def test_create_provider_fail(self): segment = {pnet.NETWORK_TYPE: None, -- 2.45.2