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
This commit is contained in:
parent
1f745f82a5
commit
db312a2886
@ -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')
|
@ -1 +1 @@
|
||||
58fe87a01143
|
||||
236b90af57ab
|
||||
|
@ -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):
|
||||
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]
|
||||
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):
|
||||
(session.query(models.NetworkSegment).
|
||||
filter_by(id=segment_id).delete())
|
||||
|
||||
|
||||
def add_port_binding(session, port_id):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user