]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Implement ML2 port binding
authorBob Kukura <rkukura@redhat.com>
Tue, 13 Aug 2013 01:58:36 +0000 (21:58 -0400)
committerBob Kukura <rkukura@redhat.com>
Tue, 3 Sep 2013 22:05:20 +0000 (18:05 -0400)
The ml2 plugin uses mechanism drivers to determine which network
segment and what VIF driver to use for a port. Mechanism drivers
supporting the openvswitch, linuxbridge, and hyperv agents are
added. The binding:host attribute is set on ports belonging to the
dhcp and l3 agents so that they can be bound.

To use with devstack until it is updated, set
"Q_ML2_PLUGIN_MECHANISM_DRIVERS=openvswitch,linuxbridge" in localrc.

The hyperv L2 agent does not currently implement the agents_db RPC,
and will therefore not work with its ml2 mechanism driver. This issue
will be tracked as a bug to be fixed in a separate merge.

implements blueprint: ml2-portbinding

Change-Id: Icb9c70d8b0d7fcb34b57adc760bb713b740e5dad

26 files changed:
neutron/common/constants.py
neutron/db/dhcp_rpc_base.py
neutron/db/l3_rpc_base.py
neutron/db/migration/alembic_migrations/versions/32a65f71af51_ml2_portbinding.py [new file with mode: 0644]
neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py
neutron/plugins/ml2/db.py
neutron/plugins/ml2/driver_api.py
neutron/plugins/ml2/driver_context.py
neutron/plugins/ml2/drivers/mech_agent.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/mech_hyperv.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/mech_linuxbridge.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/mech_openvswitch.py [new file with mode: 0644]
neutron/plugins/ml2/managers.py
neutron/plugins/ml2/models.py
neutron/plugins/ml2/plugin.py
neutron/plugins/ml2/rpc.py
neutron/plugins/openvswitch/agent/ovs_neutron_agent.py
neutron/tests/unit/ml2/_test_mech_agent.py [new file with mode: 0644]
neutron/tests/unit/ml2/drivers/mechanism_logger.py
neutron/tests/unit/ml2/drivers/mechanism_test.py
neutron/tests/unit/ml2/test_mech_hyperv.py [new file with mode: 0644]
neutron/tests/unit/ml2/test_mech_linuxbridge.py [new file with mode: 0644]
neutron/tests/unit/ml2/test_mech_openvswitch.py [new file with mode: 0644]
neutron/tests/unit/ml2/test_ml2_plugin.py
neutron/tests/unit/ml2/test_port_binding.py [new file with mode: 0644]
setup.cfg

index a41fe8553ae13b5638f0ea56c3a0c60386d4e6e5..f8ff46e4dc8cb850601e6b9e99f267c7f1e7340c 100644 (file)
@@ -67,6 +67,7 @@ TYPE_DICT = "dict"
 AGENT_TYPE_DHCP = 'DHCP agent'
 AGENT_TYPE_OVS = 'Open vSwitch agent'
 AGENT_TYPE_LINUXBRIDGE = 'Linux bridge agent'
+AGENT_TYPE_HYPERV = 'HyperV agent'
 AGENT_TYPE_NEC = 'NEC plugin agent'
 AGENT_TYPE_L3 = 'L3 agent'
 AGENT_TYPE_LOADBALANCER = 'Loadbalancer agent'
@@ -78,6 +79,7 @@ PAGINATION_INFINITE = 'infinite'
 SORT_DIRECTION_ASC = 'asc'
 SORT_DIRECTION_DESC = 'desc'
 
+PORT_BINDING_EXT_ALIAS = 'binding'
 L3_AGENT_SCHEDULER_EXT_ALIAS = 'l3_agent_scheduler'
 DHCP_AGENT_SCHEDULER_EXT_ALIAS = 'dhcp_agent_scheduler'
 LBAAS_AGENT_SCHEDULER_EXT_ALIAS = 'lbaas_agent_scheduler'
index 25a2876cdc53f736a758459a26de239d0de388a8..ee6c3db1a5d197da3168476ce45b576ff5e1c551 100644 (file)
@@ -19,6 +19,7 @@ from sqlalchemy.orm import exc
 from neutron.api.v2 import attributes
 from neutron.common import constants
 from neutron.common import utils
+from neutron.extensions import portbindings
 from neutron import manager
 from neutron.openstack.common import log as logging
 
@@ -227,6 +228,7 @@ class DhcpRpcCallbackMixin(object):
                    'host': host})
 
         port['port']['device_owner'] = constants.DEVICE_OWNER_DHCP
+        port['port'][portbindings.HOST_ID] = host
         if 'mac_address' not in port['port']:
             port['port']['mac_address'] = attributes.ATTR_NOT_SPECIFIED
         plugin = manager.NeutronManager.get_plugin()
index 1cad6c94c42c87107a6ee5ea05ef8495fa5581c2..afd64afe7a087ee5ecad7eccf398431c22c24390 100644 (file)
@@ -18,6 +18,7 @@ from oslo.config import cfg
 from neutron.common import constants
 from neutron.common import utils
 from neutron import context as neutron_context
+from neutron.extensions import portbindings
 from neutron import manager
 from neutron.openstack.common import jsonutils
 from neutron.openstack.common import log as logging
@@ -49,10 +50,31 @@ class L3RpcCallbackMixin(object):
                 context, host, router_ids)
         else:
             routers = plugin.get_sync_data(context, router_ids)
+        if utils.is_extension_supported(
+            plugin, constants.PORT_BINDING_EXT_ALIAS):
+            self._ensure_host_set_on_ports(context, plugin, host, routers)
         LOG.debug(_("Routers returned to l3 agent:\n %s"),
                   jsonutils.dumps(routers, indent=5))
         return routers
 
+    def _ensure_host_set_on_ports(self, context, plugin, host, routers):
+        for router in routers:
+            LOG.debug("checking router: %s for host: %s" %
+                      (router['id'], host))
+            self._ensure_host_set_on_port(context, plugin, host,
+                                          router.get('gw_port'))
+            for interface in router.get(constants.INTERFACE_KEY, []):
+                self._ensure_host_set_on_port(context, plugin, host,
+                                              interface)
+
+    def _ensure_host_set_on_port(self, context, plugin, host, port):
+        if (port and
+            (port.get(portbindings.HOST_ID) != host or
+             port.get(portbindings.VIF_TYPE) ==
+             portbindings.VIF_TYPE_BINDING_FAILED)):
+            plugin.update_port(context, port['id'],
+                               {'port': {portbindings.HOST_ID: host}})
+
     def get_external_network_id(self, context, **kwargs):
         """Get one external network id for l3 agent.
 
diff --git a/neutron/db/migration/alembic_migrations/versions/32a65f71af51_ml2_portbinding.py b/neutron/db/migration/alembic_migrations/versions/32a65f71af51_ml2_portbinding.py
new file mode 100644 (file)
index 0000000..6256186
--- /dev/null
@@ -0,0 +1,70 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 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 portbinding
+
+Revision ID: 32a65f71af51
+Revises: 14f24494ca31
+Create Date: 2013-09-03 08:40:22.706651
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '32a65f71af51'
+down_revision = '14f24494ca31'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+    'neutron.plugins.ml2.plugin.Ml2Plugin'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+from neutron.db import migration
+
+
+def upgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.create_table(
+        'ml2_port_bindings',
+        sa.Column('port_id', sa.String(length=36), nullable=False),
+        sa.Column('host', sa.String(length=255), nullable=False),
+        sa.Column('vif_type', sa.String(length=64), nullable=False),
+        sa.Column('cap_port_filter', sa.Boolean(), nullable=False),
+        sa.Column('driver', sa.String(length=64), nullable=True),
+        sa.Column('segment', sa.String(length=36), nullable=True),
+        sa.ForeignKeyConstraint(['port_id'], ['ports.id'],
+                                ondelete='CASCADE'),
+        sa.ForeignKeyConstraint(['segment'], ['ml2_network_segments.id'],
+                                ondelete='SET NULL'),
+        sa.PrimaryKeyConstraint('port_id')
+    )
+
+    # Note that 176a85fc7d79_add_portbindings_db.py was never enabled
+    # for ml2, so there is no need to drop the portbindingports table
+    # that is no longer used.
+
+
+def downgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.drop_table('ml2_port_bindings')
index c71f6acdbb778d0d81cdfab1f2c6687763720782..b9e4d42c11d71f263717549a213db29c2e7bf4d8 100755 (executable)
@@ -497,7 +497,7 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin):
             'binary': 'neutron-linuxbridge-agent',
             'host': cfg.CONF.host,
             'topic': constants.L2_AGENT_TOPIC,
-            'configurations': interface_mappings,
+            'configurations': {'interface_mappings': interface_mappings},
             'agent_type': constants.AGENT_TYPE_LINUXBRIDGE,
             'start_flag': True}
 
index 48c3b678e9b4206b7229cc8db92436c69ce0b0b3..c987d78602a5da1b0ca8b0af745e4d8349e283b2 100644 (file)
@@ -18,6 +18,7 @@ from sqlalchemy.orm import exc
 from neutron.db import api as db_api
 from neutron.db import models_v2
 from neutron.db import securitygroups_db as sg_db
+from neutron.extensions import portbindings
 from neutron import manager
 from neutron.openstack.common import log
 from neutron.openstack.common import uuidutils
@@ -52,12 +53,29 @@ def get_network_segments(session, network_id):
     with session.begin(subtransactions=True):
         records = (session.query(models.NetworkSegment).
                    filter_by(network_id=network_id))
-        return [{api.NETWORK_TYPE: record.network_type,
+        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]
 
 
+def ensure_port_binding(session, port_id):
+    with session.begin(subtransactions=True):
+        try:
+            record = (session.query(models.PortBinding).
+                      filter_by(port_id=port_id).
+                      one())
+        except exc.NoResultFound:
+            record = models.PortBinding(
+                port_id=port_id,
+                host='',
+                vif_type=portbindings.VIF_TYPE_UNBOUND,
+                cap_port_filter=False)
+            session.add(record)
+        return record
+
+
 def get_port(session, port_id):
     """Get port record for update within transcation."""
 
index 546c23c940d22542216c26eac18dea6f7f21ff8f..e8cb77b0bf9e6d3de720cfbf38f142ed13252e5f 100644 (file)
@@ -20,6 +20,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty
 # neutron.extensions.providernet so that drivers don't need to change
 # if/when providernet moves to the core API.
 #
+ID = 'id'
 NETWORK_TYPE = 'network_type'
 PHYSICAL_NETWORK = 'physical_network'
 SEGMENTATION_ID = 'segmentation_id'
@@ -232,6 +233,34 @@ class PortContext(object):
         """Return the NetworkContext associated with this port."""
         pass
 
+    @abstractproperty
+    def bound_segment(self):
+        """Return the currently bound segment dictionary."""
+        pass
+
+    @abstractmethod
+    def host_agents(self, agent_type):
+        """Get agents of the specified type on port's host.
+
+        :param agent_type: Agent type identifier
+        :returns: List of agents_db.Agent records
+        """
+        pass
+
+    @abstractmethod
+    def set_binding(self, segment_id, vif_type, cap_port_filter):
+        """Set the binding for the port.
+
+        :param segment_id: Network segment bound for the port.
+        :param vif_type: The VIF type for the bound port.
+        :param cap_port_filter: True if the bound port filters.
+
+        Called by MechanismDriver.bind_port to indicate success and
+        specify binding details to use for port. The segment_id must
+        identify an item in network.network_segments.
+        """
+        pass
+
 
 class MechanismDriver(object):
     """Define stable abstract interface for ML2 mechanism drivers.
@@ -530,3 +559,39 @@ class MechanismDriver(object):
         deleted.
         """
         pass
+
+    def bind_port(self, context):
+        """Attempt to bind a port.
+
+        :param context: PortContext instance describing the port
+
+        Called inside transaction context on session, prior to
+        create_network_precommit or update_network_precommit, to
+        attempt to establish a port binding. If the driver is able to
+        bind the port, it calls context.set_binding with the binding
+        details.
+        """
+        pass
+
+    def validate_port_binding(self, context):
+        """Check whether existing port binding is still valid.
+
+        :param context: PortContext instance describing the port
+        :returns: True if binding is valid, otherwise False
+
+        Called inside transaction context on session to validate that
+        the MechanismDriver's existing binding for the port is still
+        valid.
+        """
+        return False
+
+    def unbind_port(self, context):
+        """Undo existing port binding.
+
+        :param context: PortContext instance describing the port
+
+        Called inside transaction context on session to notify the
+        MechanismDriver that its existing binding for the port is no
+        longer valid.
+        """
+        pass
index 1f552d2d82d7e3870ec293d6cb8dc4037a48c172..7468192f7472c97aa654b421959c4e83db212d1e 100644 (file)
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron.plugins.ml2 import db
 from neutron.plugins.ml2 import driver_api as api
 
 
@@ -29,11 +30,12 @@ class MechanismDriverContext(object):
 class NetworkContext(MechanismDriverContext, api.NetworkContext):
 
     def __init__(self, plugin, plugin_context, network,
-                 segments=None, original_network=None):
+                 original_network=None):
         super(NetworkContext, self).__init__(plugin, plugin_context)
         self._network = network
         self._original_network = original_network
-        self._segments = segments
+        self._segments = db.get_network_segments(plugin_context.session,
+                                                 network['id'])
 
     @property
     def current(self):
@@ -45,9 +47,6 @@ class NetworkContext(MechanismDriverContext, api.NetworkContext):
 
     @property
     def network_segments(self):
-        if not self._segments:
-            self._segments = self._plugin.get_network_segments(
-                self._plugin_context, self._network['id'])
         return self._segments
 
 
@@ -69,12 +68,15 @@ class SubnetContext(MechanismDriverContext, api.SubnetContext):
 
 class PortContext(MechanismDriverContext, api.PortContext):
 
-    def __init__(self, plugin, plugin_context, port,
+    def __init__(self, plugin, plugin_context, port, network,
                  original_port=None):
         super(PortContext, self).__init__(plugin, plugin_context)
         self._port = port
         self._original_port = original_port
-        self._network_context = None
+        self._network_context = NetworkContext(plugin, plugin_context,
+                                               network)
+        self._binding = db.ensure_port_binding(plugin_context.session,
+                                               port['id'])
 
     @property
     def current(self):
@@ -86,11 +88,27 @@ class PortContext(MechanismDriverContext, api.PortContext):
 
     @property
     def network(self):
-        """Return the NetworkContext associated with this port."""
-        if not self._network_context:
-            network = self._plugin.get_network(self._plugin_context,
-                                               self._port["network_id"])
-            self._network_context = NetworkContext(self._plugin,
-                                                   self._plugin_context,
-                                                   network)
         return self._network_context
+
+    @property
+    def bound_segment(self):
+        id = self._binding.segment
+        if id:
+            for segment in self._network_context.network_segments:
+                if segment[api.ID] == id:
+                    return segment
+
+    def host_agents(self, agent_type):
+        return self._plugin.get_agents(self._plugin_context,
+                                       filters={'agent_type': [agent_type],
+                                                'host': [self._binding.host]})
+
+    def set_binding(self, segment_id, vif_type, cap_port_filter):
+        # REVISIT(rkukura): Pass extensible list of capabilities? Move
+        # vif_type and capabilities to methods on the bound mechanism
+        # driver?
+
+        # TODO(rkukura) Verify binding allowed, segment in network
+        self._binding.segment = segment_id
+        self._binding.vif_type = vif_type
+        self._binding.cap_port_filter = cap_port_filter
diff --git a/neutron/plugins/ml2/drivers/mech_agent.py b/neutron/plugins/ml2/drivers/mech_agent.py
new file mode 100644 (file)
index 0000000..a89956f
--- /dev/null
@@ -0,0 +1,105 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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 abc import ABCMeta, abstractmethod
+
+from neutron.openstack.common import log
+from neutron.plugins.ml2 import driver_api as api
+
+LOG = log.getLogger(__name__)
+
+
+class AgentMechanismDriverBase(api.MechanismDriver):
+    """Base class for drivers that attach to networks using an L2 agent.
+
+    The AgentMechanismDriverBase provides common code for mechanism
+    drivers that integrate the ml2 plugin with L2 agents. Port binding
+    with this driver requires the driver's associated agent to be
+    running on the port's host, and that agent to have connectivity to
+    at least one segment of the port's network.
+
+    MechanismDrivers using this base class must pass the agent type
+    and VIF type constants to __init__(), and must implement
+    check_segment_for_agent().
+    """
+
+    __metaclass__ = ABCMeta
+
+    def __init__(self, agent_type, vif_type, cap_port_filter):
+        """Initialize base class for specific L2 agent type.
+
+        :param agent_type: Constant identifying agent type in agents_db
+        :param vif_type: Value for binding:vif_type to when bound
+        """
+        self.agent_type = agent_type
+        self.vif_type = vif_type
+        self.cap_port_filter = cap_port_filter
+
+    def initialize(self):
+        pass
+
+    def bind_port(self, context):
+        LOG.debug(_("Attempting to bind port %(port)s on "
+                    "network %(network)s"),
+                  {'port': context.current['id'],
+                   'network': context.network.current['id']})
+        for agent in context.host_agents(self.agent_type):
+            LOG.debug(_("Checking agent: %s"), agent)
+            if agent['alive']:
+                for segment in context.network.network_segments:
+                    if self.check_segment_for_agent(segment, agent):
+                        context.set_binding(segment[api.ID],
+                                            self.vif_type,
+                                            self.cap_port_filter)
+                        LOG.debug(_("Bound using segment: %s"), segment)
+                        return
+            else:
+                LOG.warning(_("Attempting to bind with dead agent: %s"),
+                            agent)
+
+    def validate_port_binding(self, context):
+        LOG.debug(_("Validating binding for port %(port)s on "
+                    "network %(network)s"),
+                  {'port': context.current['id'],
+                   'network': context.network.current['id']})
+        for agent in context.host_agents(self.agent_type):
+            LOG.debug(_("Checking agent: %s"), agent)
+            if agent['alive'] and self.check_segment_for_agent(
+                context.bound_segment, agent):
+                LOG.debug(_("Binding valid"))
+                return True
+        LOG.warning(_("Binding invalid for port: %s"), context.current)
+        return False
+
+    def unbind_port(self, context):
+        LOG.debug(_("Unbinding port %(port)s on "
+                    "network %(network)s"),
+                  {'port': context.current['id'],
+                   'network': context.network.current['id']})
+
+    @abstractmethod
+    def check_segment_for_agent(self, segment, agent):
+        """Check if segment can be bound for agent.
+
+        :param segment: segment dictionary describing segment to bind
+        :param agent: agents_db entry describing agent to bind
+        :returns: True iff segment can be bound for agent
+
+        Called inside transaction during bind_port() and
+        validate_port_binding() so that derived MechanismDrivers can
+        use agent_db data along with built-in knowledge of the
+        corresponding agent's capabilities to determine whether or not
+        the specified network segment can be bound for the agent.
+        """
diff --git a/neutron/plugins/ml2/drivers/mech_hyperv.py b/neutron/plugins/ml2/drivers/mech_hyperv.py
new file mode 100644 (file)
index 0000000..a6c8d42
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.openstack.common import log
+from neutron.plugins.ml2 import driver_api as api
+from neutron.plugins.ml2.drivers import mech_agent
+
+LOG = log.getLogger(__name__)
+
+
+class HypervMechanismDriver(mech_agent.AgentMechanismDriverBase):
+    """Attach to networks using hyperv L2 agent.
+
+    The HypervMechanismDriver integrates the ml2 plugin with the
+    hyperv L2 agent. Port binding with this driver requires the hyperv
+    agent to be running on the port's host, and that agent to have
+    connectivity to at least one segment of the port's network.
+    """
+
+    def __init__(self):
+        super(HypervMechanismDriver, self).__init__(
+            constants.AGENT_TYPE_HYPERV,
+            portbindings.VIF_TYPE_HYPERV,
+            False)
+
+    def check_segment_for_agent(self, segment, agent):
+        mappings = agent['configurations'].get('vswitch_mappings', {})
+        LOG.debug(_("Checking segment: %(segment)s "
+                    "for mappings: %(mappings)s"),
+                  {'segment': segment, 'mappings': mappings})
+        network_type = segment[api.NETWORK_TYPE]
+        if network_type == 'local':
+            return True
+        elif network_type in ['flat', 'vlan']:
+            return segment[api.PHYSICAL_NETWORK] in mappings
+        else:
+            return False
diff --git a/neutron/plugins/ml2/drivers/mech_linuxbridge.py b/neutron/plugins/ml2/drivers/mech_linuxbridge.py
new file mode 100644 (file)
index 0000000..15ca7d6
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.openstack.common import log
+from neutron.plugins.ml2 import driver_api as api
+from neutron.plugins.ml2.drivers import mech_agent
+
+LOG = log.getLogger(__name__)
+
+
+class LinuxbridgeMechanismDriver(mech_agent.AgentMechanismDriverBase):
+    """Attach to networks using linuxbridge L2 agent.
+
+    The LinuxbridgeMechanismDriver integrates the ml2 plugin with the
+    linuxbridge L2 agent. Port binding with this driver requires the
+    linuxbridge agent to be running on the port's host, and that agent
+    to have connectivity to at least one segment of the port's
+    network.
+    """
+
+    def __init__(self):
+        super(LinuxbridgeMechanismDriver, self).__init__(
+            constants.AGENT_TYPE_LINUXBRIDGE,
+            portbindings.VIF_TYPE_BRIDGE,
+            True)
+
+    def check_segment_for_agent(self, segment, agent):
+        mappings = agent['configurations'].get('interface_mappings', {})
+        LOG.debug(_("Checking segment: %(segment)s "
+                    "for mappings: %(mappings)s"),
+                  {'segment': segment, 'mappings': mappings})
+        network_type = segment[api.NETWORK_TYPE]
+        if network_type == 'local':
+            return True
+        elif network_type in ['flat', 'vlan']:
+            return segment[api.PHYSICAL_NETWORK] in mappings
+        else:
+            return False
diff --git a/neutron/plugins/ml2/drivers/mech_openvswitch.py b/neutron/plugins/ml2/drivers/mech_openvswitch.py
new file mode 100644 (file)
index 0000000..f98ddf2
--- /dev/null
@@ -0,0 +1,57 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.openstack.common import log
+from neutron.plugins.ml2 import driver_api as api
+from neutron.plugins.ml2.drivers import mech_agent
+
+LOG = log.getLogger(__name__)
+
+
+class OpenvswitchMechanismDriver(mech_agent.AgentMechanismDriverBase):
+    """Attach to networks using openvswitch L2 agent.
+
+    The OpenvswitchMechanismDriver integrates the ml2 plugin with the
+    openvswitch L2 agent. Port binding with this driver requires the
+    openvswitch agent to be running on the port's host, and that agent
+    to have connectivity to at least one segment of the port's
+    network.
+    """
+
+    def __init__(self):
+        super(OpenvswitchMechanismDriver, self).__init__(
+            constants.AGENT_TYPE_OVS,
+            portbindings.VIF_TYPE_OVS,
+            True)
+
+    def check_segment_for_agent(self, segment, agent):
+        mappings = agent['configurations'].get('bridge_mappings', {})
+        tunnel_types = agent['configurations'].get('tunnel_types', [])
+        LOG.debug(_("Checking segment: %(segment)s "
+                    "for mappings: %(mappings)s "
+                    "with tunnel_types: %(tunnel_types)s"),
+                  {'segment': segment, 'mappings': mappings,
+                   'tunnel_types': tunnel_types})
+        network_type = segment[api.NETWORK_TYPE]
+        if network_type == 'local':
+            return True
+        elif network_type in tunnel_types:
+            return True
+        elif network_type in ['flat', 'vlan']:
+            return segment[api.PHYSICAL_NETWORK] in mappings
+        else:
+            return False
index 54321cf7267cff474e33de567e83b983cc033fbd..e25d79d5a12b65eaec1da1c26719e266edf4d5de 100644 (file)
@@ -19,6 +19,7 @@ from oslo.config import cfg
 import stevedore
 
 from neutron.common import exceptions as exc
+from neutron.extensions import portbindings
 from neutron.openstack.common import log
 from neutron.plugins.ml2.common import exceptions as ml2_exc
 from neutron.plugins.ml2 import driver_api as api
@@ -31,12 +32,9 @@ class TypeManager(stevedore.named.NamedExtensionManager):
     """Manage network segment types using drivers."""
 
     def __init__(self):
-        # REVISIT(rkukura): Need way to make stevedore use our logging
-        # configuration. Currently, nothing is logged if loading a
-        # driver fails.
-
         # Mapping from type name to DriverManager
         self.drivers = {}
+
         LOG.info(_("Configured type driver names: %s"),
                  cfg.CONF.ml2.type_drivers)
         super(TypeManager, self).__init__('neutron.ml2.type_drivers',
@@ -106,19 +104,9 @@ class TypeManager(stevedore.named.NamedExtensionManager):
 
 
 class MechanismManager(stevedore.named.NamedExtensionManager):
-    """Manage networking mechanisms using drivers.
-
-    Note that this is still a work in progress, and the interface
-    may change before the final release of Havana.
-    """
-
-    # TODO(apech): add calls for subnets
+    """Manage networking mechanisms using drivers."""
 
     def __init__(self):
-        # REVISIT(rkukura): Need way to make stevedore use our logging
-        # configuration. Currently, nothing is logged if loading a
-        # driver fails.
-
         # Registered mechanism drivers, keyed by name.
         self.mech_drivers = {}
         # Ordered list of mechanism drivers, defining
@@ -435,3 +423,85 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
         """
         self._call_on_drivers("delete_port_postcommit", context,
                               continue_on_failure=True)
+
+    def bind_port(self, context):
+        """Attempt to bind a port using registered mechanism drivers.
+
+        :param context: PortContext instance describing the port
+
+        Called inside transaction context on session, prior to
+        create_network_precommit or update_network_precommit, to
+        attempt to establish a port binding.
+        """
+        binding = context._binding
+        LOG.debug(_("Attempting to bind port %(port)s on host %(host)s"),
+                  {'port': context._port['id'],
+                   'host': binding.host})
+        for driver in self.ordered_mech_drivers:
+            try:
+                driver.obj.bind_port(context)
+                if binding.segment:
+                    binding.driver = driver.name
+                    LOG.debug(_("Bound port: %(port)s, host: %(host)s, "
+                                "driver: %(driver)s, vif_type: %(vif_type)s, "
+                                "cap_port_filter: %(cap_port_filter)s, "
+                                "segment: %(segment)s"),
+                              {'port': context._port['id'],
+                               'host': binding.host,
+                               'driver': binding.driver,
+                               'vif_type': binding.vif_type,
+                               'cap_port_filter': binding.cap_port_filter,
+                               'segment': binding.segment})
+                    return
+            except Exception:
+                LOG.exception(_("Mechanism driver %s failed in "
+                                "bind_port"),
+                              driver.name)
+        binding.vif_type = portbindings.VIF_TYPE_BINDING_FAILED
+        LOG.warning(_("Failed to bind port %(port)s on host %(host)s"),
+                    {'port': context._port['id'],
+                     'host': binding.host})
+
+    def validate_port_binding(self, context):
+        """Check whether existing port binding is still valid.
+
+        :param context: PortContext instance describing the port
+        :returns: True if binding is valid, otherwise False
+
+        Called inside transaction context on session to validate that
+        the bound MechanismDriver's existing binding for the port is
+        still valid.
+        """
+        binding = context._binding
+        driver = self.mech_drivers.get(binding.driver, None)
+        if driver:
+            try:
+                return driver.obj.validate_port_binding(context)
+            except Exception:
+                LOG.exception(_("Mechanism driver %s failed in "
+                                "validate_port_binding"),
+                              driver.name)
+        return False
+
+    def unbind_port(self, context):
+        """Undo existing port binding.
+
+        :param context: PortContext instance describing the port
+
+        Called inside transaction context on session to notify the
+        bound MechanismDriver that its existing binding for the port
+        is no longer valid.
+        """
+        binding = context._binding
+        driver = self.mech_drivers.get(binding.driver, None)
+        if driver:
+            try:
+                driver.obj.unbind_port(context)
+            except Exception:
+                LOG.exception(_("Mechanism driver %s failed in "
+                                "unbind_port"),
+                              driver.name)
+        binding.vif_type = portbindings.VIF_TYPE_UNBOUND
+        binding.cap_port_filter = False
+        binding.driver = None
+        binding.segment = None
index ba5650940ca1879761fbd9d3841dc2dab2188fcf..9b215be2740e9e0c8813d0e57001edda74f9e8a1 100644 (file)
@@ -14,6 +14,7 @@
 #    under the License.
 
 import sqlalchemy as sa
+from sqlalchemy import orm
 
 from neutron.db import model_base
 from neutron.db import models_v2
@@ -35,3 +36,34 @@ 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)
+
+
+class PortBinding(model_base.BASEV2):
+    """Represent binding-related state of a port.
+
+    A port binding stores the port attributes required for the
+    portbindings extension, as well as internal ml2 state such as
+    which MechanismDriver and which segment are used by the port
+    binding.
+    """
+
+    __tablename__ = 'ml2_port_bindings'
+
+    port_id = sa.Column(sa.String(36),
+                        sa.ForeignKey('ports.id', ondelete="CASCADE"),
+                        primary_key=True)
+    host = sa.Column(sa.String(255), nullable=False)
+    vif_type = sa.Column(sa.String(64), nullable=False)
+    cap_port_filter = sa.Column(sa.Boolean, nullable=False)
+    driver = sa.Column(sa.String(64))
+    segment = sa.Column(sa.String(36),
+                        sa.ForeignKey('ml2_network_segments.id',
+                                      ondelete="SET NULL"))
+
+    # Add a relationship to the Port model in order to instruct SQLAlchemy to
+    # eagerly load port bindings
+    port = orm.relationship(
+        models_v2.Port,
+        backref=orm.backref("port_binding",
+                            lazy='joined', uselist=False,
+                            cascade='delete'))
index c14243dddb6ac5de648f7c8ca4e96a57fb932c43..892685ef5c7bbb6c172fb12d5d85cfdc98ee088b 100644 (file)
@@ -26,7 +26,7 @@ from neutron.db import agentschedulers_db
 from neutron.db import db_base_plugin_v2
 from neutron.db import extraroute_db
 from neutron.db import l3_gwmode_db
-from neutron.db import portbindings_db
+from neutron.db import models_v2
 from neutron.db import quota_db  # noqa
 from neutron.db import securitygroups_rpc_base as sg_db_rpc
 from neutron.extensions import portbindings
@@ -41,6 +41,7 @@ from neutron.plugins.ml2 import db
 from neutron.plugins.ml2 import driver_api as api
 from neutron.plugins.ml2 import driver_context
 from neutron.plugins.ml2 import managers
+from neutron.plugins.ml2 import models
 from neutron.plugins.ml2 import rpc
 
 LOG = log.getLogger(__name__)
@@ -55,8 +56,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                 l3_gwmode_db.L3_NAT_db_mixin,
                 sg_db_rpc.SecurityGroupServerRpcMixin,
                 agentschedulers_db.L3AgentSchedulerDbMixin,
-                agentschedulers_db.DhcpAgentSchedulerDbMixin,
-                portbindings_db.PortBindingMixin):
+                agentschedulers_db.DhcpAgentSchedulerDbMixin):
     """Implement the Neutron L2 abstractions using modules.
 
     Ml2Plugin is a Neutron plugin based on separately extensible sets
@@ -151,7 +151,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
 
     def _extend_network_dict_provider(self, context, network):
         id = network['id']
-        segments = self.get_network_segments(context, id)
+        segments = db.get_network_segments(context.session, id)
         if not segments:
             LOG.error(_("Network %s has no segments"), id)
             network[provider.NETWORK_TYPE] = None
@@ -171,28 +171,88 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
         # TODO(rkukura): Implement filtering.
         return nets
 
-    def _extend_port_dict_binding(self, context, port):
-        # TODO(rkukura): Implement based on host_id, agents, and
-        # MechanismDrivers. Also set CAPABILITIES. Use
-        # base_binding_dict if applicable, or maybe a new hook so
-        # base handles field processing and get_port and get_ports
-        # don't need to be overridden.
-        port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_UNBOUND
-
-    def _notify_port_updated(self, context, port):
-        session = context.session
-        with session.begin(subtransactions=True):
-            network_id = port['network_id']
-            segments = self.get_network_segments(context, network_id)
-            if not segments:
-                LOG.warning(_("In _notify_port_updated() for port %(port_id), "
-                              "network %(network_id) has no segments"),
-                            {'port_id': port['id'],
-                             'network_id': network_id})
-                return
-            # TODO(rkukura): Use port binding to select segment.
-            segment = segments[0]
-        self.notifier.port_update(context, port,
+    def _process_port_binding(self, mech_context, attrs):
+        binding = mech_context._binding
+        port = mech_context.current
+        self._update_port_dict_binding(port, binding)
+
+        host = attrs and attrs.get(portbindings.HOST_ID)
+        host_set = attributes.is_attr_set(host)
+
+        if binding.vif_type != portbindings.VIF_TYPE_UNBOUND:
+            if (not host_set and binding.segment and
+                self.mechanism_manager.validate_port_binding(mech_context)):
+                return False
+            self.mechanism_manager.unbind_port(mech_context)
+            self._update_port_dict_binding(port, binding)
+
+        if host_set:
+            binding.host = host
+            port[portbindings.HOST_ID] = host
+
+        if binding.host:
+            self.mechanism_manager.bind_port(mech_context)
+            self._update_port_dict_binding(port, binding)
+
+        return True
+
+    def _update_port_dict_binding(self, port, binding):
+        port[portbindings.HOST_ID] = binding.host
+        port[portbindings.VIF_TYPE] = binding.vif_type
+        port[portbindings.CAPABILITIES] = {
+            portbindings.CAP_PORT_FILTER: binding.cap_port_filter}
+
+    def _delete_port_binding(self, mech_context):
+        binding = mech_context._binding
+        port = mech_context.current
+        self._update_port_dict_binding(port, binding)
+        self.mechanism_manager.unbind_port(mech_context)
+        self._update_port_dict_binding(port, binding)
+
+    def _extend_port_dict_binding(self, port_res, port_db):
+        # None when called during unit tests for other plugins.
+        if port_db.port_binding:
+            self._update_port_dict_binding(port_res, port_db.port_binding)
+
+    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
+        attributes.PORTS, [_extend_port_dict_binding])
+
+    # Note - The following hook methods have "ml2" in their names so
+    # that they are not called twice during unit tests due to global
+    # registration of hooks in portbindings_db.py used by other
+    # plugins.
+
+    def _ml2_port_model_hook(self, context, original_model, query):
+        query = query.outerjoin(models.PortBinding,
+                                (original_model.id ==
+                                 models.PortBinding.port_id))
+        return query
+
+    def _ml2_port_result_filter_hook(self, query, filters):
+        values = filters and filters.get(portbindings.HOST_ID, [])
+        if not values:
+            return query
+        return query.filter(models.PortBinding.host.in_(values))
+
+    db_base_plugin_v2.NeutronDbPluginV2.register_model_query_hook(
+        models_v2.Port,
+        "ml2_port_bindings",
+        '_ml2_port_model_hook',
+        None,
+        '_ml2_port_result_filter_hook')
+
+    def _notify_port_updated(self, mech_context):
+        port = mech_context._port
+        segment = mech_context.bound_segment
+        if not segment:
+            # REVISIT(rkukura): This should notify agent to unplug port
+            network = mech_context.network.current
+            LOG.warning(_("In _notify_port_updated(), no bound segment for "
+                          "port %(port_id)s on network %(network_id)s"),
+                        {'port_id': port['id'],
+                         'network_id': network['id']})
+            return
+        self.notifier.port_update(mech_context._plugin_context, port,
                                   segment[api.NETWORK_TYPE],
                                   segment[api.SEGMENTATION_ID],
                                   segment[api.PHYSICAL_NETWORK])
@@ -218,10 +278,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
             # to TypeManager.
             db.add_network_segment(session, id, segment)
             self._extend_network_dict_provider(context, result)
-            mech_context = driver_context.NetworkContext(self,
-                                                         context,
-                                                         result,
-                                                         segments=[segment])
+            mech_context = driver_context.NetworkContext(self, context,
+                                                         result)
             self.mechanism_manager.create_network_precommit(mech_context)
 
         try:
@@ -280,24 +338,15 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
 
         return [self._fields(net, fields) for net in nets]
 
-    def get_network_segments(self, context, id):
-        session = context.session
-        with session.begin(subtransactions=True):
-            segments = db.get_network_segments(session, id)
-        return segments
-
     def delete_network(self, context, id):
         session = context.session
         with session.begin(subtransactions=True):
             network = self.get_network(context, id)
-            segments = self.get_network_segments(context, id)
-            mech_context = driver_context.NetworkContext(self,
-                                                         context,
-                                                         network,
-                                                         segments=segments)
+            mech_context = driver_context.NetworkContext(self, context,
+                                                         network)
             self.mechanism_manager.delete_network_precommit(mech_context)
             super(Ml2Plugin, self).delete_network(context, id)
-            for segment in segments:
+            for segment in mech_context.network_segments:
                 self.type_manager.release_segment(session, segment)
             # The segment records are deleted via cascade from the
             # network record, so explicit removal is not necessary.
@@ -368,11 +417,11 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
             self._ensure_default_security_group_on_port(context, port)
             sgids = self._get_security_groups_on_port(context, port)
             result = super(Ml2Plugin, self).create_port(context, port)
-            self._process_portbindings_create_and_update(context, attrs,
-                                                         result)
             self._process_port_create_security_group(context, result, sgids)
-            self._extend_port_dict_binding(context, result)
-            mech_context = driver_context.PortContext(self, context, result)
+            network = self.get_network(context, result['network_id'])
+            mech_context = driver_context.PortContext(self, context, result,
+                                                      network)
+            self._process_port_binding(mech_context, attrs)
             self.mechanism_manager.create_port_precommit(mech_context)
 
         try:
@@ -396,13 +445,12 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                                                               port)
             need_port_update_notify = self.update_security_group_on_port(
                 context, id, port, original_port, updated_port)
-            self._process_portbindings_create_and_update(context,
-                                                         attrs,
-                                                         updated_port)
-            self._extend_port_dict_binding(context, updated_port)
+            network = self.get_network(context, original_port['network_id'])
             mech_context = driver_context.PortContext(
-                self, context, updated_port,
+                self, context, updated_port, network,
                 original_port=original_port)
+            need_port_update_notify |= self._process_port_binding(
+                mech_context, attrs)
             self.mechanism_manager.update_port_precommit(mech_context)
 
         # TODO(apech) - handle errors raised by update_port, potentially
@@ -418,31 +466,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
             need_port_update_notify = True
 
         if need_port_update_notify:
-            self._notify_port_updated(context, updated_port)
+            self._notify_port_updated(mech_context)
 
         return updated_port
 
-    def get_port(self, context, id, fields=None):
-        session = context.session
-        with session.begin(subtransactions=True):
-            port = super(Ml2Plugin, self).get_port(context, id, fields)
-            self._extend_port_dict_binding(context, port)
-
-        return self._fields(port, fields)
-
-    def get_ports(self, context, filters=None, fields=None,
-                  sorts=None, limit=None, marker=None, page_reverse=False):
-        session = context.session
-        with session.begin(subtransactions=True):
-            ports = super(Ml2Plugin,
-                          self).get_ports(context, filters, fields, sorts,
-                                          limit, marker, page_reverse)
-            # TODO(nati): filter by security group
-            for port in ports:
-                self._extend_port_dict_binding(context, port)
-
-        return [self._fields(port, fields) for port in ports]
-
     def delete_port(self, context, id, l3_port_check=True):
         if l3_port_check:
             self.prevent_l3_port_deletion(context, id)
@@ -451,7 +478,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
         with session.begin(subtransactions=True):
             self.disassociate_floatingips(context, id)
             port = self.get_port(context, id)
-            mech_context = driver_context.PortContext(self, context, port)
+            network = self.get_network(context, port['network_id'])
+            mech_context = driver_context.PortContext(self, context, port,
+                                                      network)
+            self._delete_port_binding(mech_context)
             self.mechanism_manager.delete_port_precommit(mech_context)
             self._delete_port_security_group_bindings(context, id)
             super(Ml2Plugin, self).delete_port(context, id)
index 9b26bcfd9bf8bddbca16b2cfabeea01236fb87de..d19995d740150d994473b5d6f541a85f1760038e 100644 (file)
@@ -97,6 +97,7 @@ class RpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin,
                               "%(agent_id)s not found in database"),
                             {'device': device, 'agent_id': agent_id})
                 return {'device': device}
+
             segments = db.get_network_segments(session, port.network_id)
             if not segments:
                 LOG.warning(_("Device %(device)s requested by agent "
@@ -106,8 +107,29 @@ class RpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin,
                              'agent_id': agent_id,
                              'network_id': port.network_id})
                 return {'device': device}
-            #TODO(rkukura): Use/create port binding
-            segment = segments[0]
+
+            binding = db.ensure_port_binding(session, port.id)
+            if not binding.segment:
+                LOG.warning(_("Device %(device)s requested by agent "
+                              "%(agent_id)s on network %(network_id)s not "
+                              "bound, vif_type: %(vif_type)s"),
+                            {'device': device,
+                             'agent_id': agent_id,
+                             'network_id': port.network_id,
+                             'vif_type': binding.vif_type})
+                return {'device': device}
+
+            segment = self._find_segment(segments, binding.segment)
+            if not segment:
+                LOG.warning(_("Device %(device)s requested by agent "
+                              "%(agent_id)s on network %(network_id)s "
+                              "invalid segment, vif_type: %(vif_type)s"),
+                            {'device': device,
+                             'agent_id': agent_id,
+                             'network_id': port.network_id,
+                             'vif_type': binding.vif_type})
+                return {'device': device}
+
             new_status = (q_const.PORT_STATUS_ACTIVE if port.admin_state_up
                           else q_const.PORT_STATUS_DOWN)
             if port.status != new_status:
@@ -122,6 +144,11 @@ class RpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin,
             LOG.debug(_("Returning: %s"), entry)
             return entry
 
+    def _find_segment(self, segments, segment_id):
+        for segment in segments:
+            if segment[api.ID] == segment_id:
+                return segment
+
     def update_device_down(self, rpc_context, **kwargs):
         """Device no longer exists on agent."""
         # TODO(garyk) - live migration and port status
index 36caea1e0a38992c395b3fe5fee21682075f654d..b89faa905f417cb438db82338926119a06f06ef3 100644 (file)
@@ -174,9 +174,9 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin):
             'binary': 'neutron-openvswitch-agent',
             'host': cfg.CONF.host,
             'topic': q_const.L2_AGENT_TOPIC,
-            'configurations': bridge_mappings,
+            'configurations': {'bridge_mappings': bridge_mappings,
+                               'tunnel_types': self.tunnel_types},
             'agent_type': q_const.AGENT_TYPE_OVS,
-            'tunnel_types': self.tunnel_types,
             'start_flag': True}
 
         self.int_br = ovs_lib.OVSBridge(integ_br, self.root_helper)
diff --git a/neutron/tests/unit/ml2/_test_mech_agent.py b/neutron/tests/unit/ml2/_test_mech_agent.py
new file mode 100644 (file)
index 0000000..83e772b
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.plugins.ml2 import driver_api as api
+from neutron.tests import base
+
+NETWORK_ID = "fake_network"
+PORT_ID = "fake_port"
+
+
+class FakeNetworkContext(api.NetworkContext):
+    def __init__(self, segments):
+        self._network_segments = segments
+
+    @property
+    def current(self):
+        return {'id': NETWORK_ID}
+
+    @property
+    def original(self):
+        return None
+
+    @property
+    def network_segments(self):
+        return self._network_segments
+
+
+class FakePortContext(api.PortContext):
+    def __init__(self, agent_type, agents, segments):
+        self._agent_type = agent_type
+        self._agents = agents
+        self._network_context = FakeNetworkContext(segments)
+        self._bound_segment_id = None
+        self._bound_vif_type = None
+        self._bound_cap_port_filter = None
+
+    @property
+    def current(self):
+        return {'id': PORT_ID}
+
+    @property
+    def original(self):
+        return None
+
+    @property
+    def network(self):
+        return self._network_context
+
+    @property
+    def bound_segment(self):
+        if self._bound_segment_id:
+            for segment in self._network_context.network_segments:
+                if segment[api.ID] == self._bound_segment_id:
+                    return segment
+
+    def host_agents(self, agent_type):
+        if agent_type == self._agent_type:
+            return self._agents
+        else:
+            return []
+
+    def set_binding(self, segment_id, vif_type, cap_port_filter):
+        self._bound_segment_id = segment_id
+        self._bound_vif_type = vif_type
+        self._bound_cap_port_filter = cap_port_filter
+
+
+class AgentMechanismBaseTestCase(base.BaseTestCase):
+    # These following must be overriden for the specific mechanism
+    # driver being tested:
+    VIF_TYPE = None
+    CAP_PORT_FILTER = None
+    AGENT_TYPE = None
+    AGENTS = None
+    AGENTS_DEAD = None
+    AGENTS_BAD = None
+
+    def _check_unbound(self, context):
+        self.assertIsNone(context._bound_segment_id)
+        self.assertIsNone(context._bound_vif_type)
+        self.assertIsNone(context._bound_cap_port_filter)
+
+    def _check_bound(self, context, segment):
+        self.assertEqual(context._bound_segment_id, segment[api.ID])
+        self.assertEqual(context._bound_vif_type, self.VIF_TYPE)
+        self.assertEqual(context._bound_cap_port_filter, self.CAP_PORT_FILTER)
+
+
+class AgentMechanismGenericTestCase(AgentMechanismBaseTestCase):
+    UNKNOWN_TYPE_SEGMENTS = [{api.ID: 'unknown_segment_id',
+                              api.NETWORK_TYPE: 'no_such_type'}]
+
+    def test_unknown_type(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS,
+                                  self.UNKNOWN_TYPE_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_unbound(context)
+
+
+class AgentMechanismLocalTestCase(AgentMechanismBaseTestCase):
+    LOCAL_SEGMENTS = [{api.ID: 'unknown_segment_id',
+                       api.NETWORK_TYPE: 'no_such_type'},
+                      {api.ID: 'local_segment_id',
+                       api.NETWORK_TYPE: 'local'}]
+
+    def test_type_local(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS,
+                                  self.LOCAL_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_bound(context, self.LOCAL_SEGMENTS[1])
+        self.assertTrue(self.driver.validate_port_binding(context))
+        self.driver.unbind_port(context)
+
+    def test_type_local_dead(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS_DEAD,
+                                  self.LOCAL_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_unbound(context)
+
+
+class AgentMechanismFlatTestCase(AgentMechanismBaseTestCase):
+    FLAT_SEGMENTS = [{api.ID: 'unknown_segment_id',
+                      api.NETWORK_TYPE: 'no_such_type'},
+                     {api.ID: 'flat_segment_id',
+                      api.NETWORK_TYPE: 'flat',
+                      api.PHYSICAL_NETWORK: 'fake_physical_network'}]
+
+    def test_type_flat(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS,
+                                  self.FLAT_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_bound(context, self.FLAT_SEGMENTS[1])
+        self.assertTrue(self.driver.validate_port_binding(context))
+        self.driver.unbind_port(context)
+
+    def test_type_flat_bad(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS_BAD,
+                                  self.FLAT_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_unbound(context)
+
+
+class AgentMechanismVlanTestCase(AgentMechanismBaseTestCase):
+    VLAN_SEGMENTS = [{api.ID: 'unknown_segment_id',
+                      api.NETWORK_TYPE: 'no_such_type'},
+                     {api.ID: 'vlan_segment_id',
+                      api.NETWORK_TYPE: 'vlan',
+                      api.PHYSICAL_NETWORK: 'fake_physical_network',
+                      api.SEGMENTATION_ID: 1234}]
+
+    def test_type_vlan(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS,
+                                  self.VLAN_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_bound(context, self.VLAN_SEGMENTS[1])
+        self.assertTrue(self.driver.validate_port_binding(context))
+        self.driver.unbind_port(context)
+
+    def test_type_vlan_bad(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS_BAD,
+                                  self.VLAN_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_unbound(context)
+
+
+class AgentMechanismGreTestCase(AgentMechanismBaseTestCase):
+    GRE_SEGMENTS = [{api.ID: 'unknown_segment_id',
+                     api.NETWORK_TYPE: 'no_such_type'},
+                    {api.ID: 'gre_segment_id',
+                     api.NETWORK_TYPE: 'gre',
+                     api.SEGMENTATION_ID: 1234}]
+
+    def test_type_gre(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS,
+                                  self.GRE_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_bound(context, self.GRE_SEGMENTS[1])
+        self.assertTrue(self.driver.validate_port_binding(context))
+        self.driver.unbind_port(context)
+
+    def test_type_gre_bad(self):
+        context = FakePortContext(self.AGENT_TYPE,
+                                  self.AGENTS_BAD,
+                                  self.GRE_SEGMENTS)
+        self.driver.bind_port(context)
+        self._check_unbound(context)
index c900fdfe10836edd76e383484604d0d72970e1dc..ca92dbc648ac54f60916c5640611478dc669d3b4 100644 (file)
@@ -87,6 +87,7 @@ class LoggerMechanismDriver(api.MechanismDriver):
                  {'method': method_name,
                   'current': context.current,
                   'original': context.original,
+                  'segment': context.bound_segment,
                   'network': network_context.current})
 
     def create_port_precommit(self, context):
@@ -106,3 +107,12 @@ class LoggerMechanismDriver(api.MechanismDriver):
 
     def delete_port_postcommit(self, context):
         self._log_port_call("delete_port_postcommit", context)
+
+    def bind_port(self, context):
+        self._log_port_call("bind_port", context)
+
+    def validate_port_binding(self, context):
+        self._log_port_call("validate_port_binding", context)
+
+    def unbind_port(self, context):
+        self._log_port_call("unbind_port", context)
index bc1ce6f6df6bc745542e5760132feea840c7898f..24a40550456ab5271587cd4a353b52306414b92f 100644 (file)
@@ -13,8 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron.extensions import portbindings
 from neutron.plugins.ml2 import driver_api as api
-from neutron.plugins.ml2 import driver_context
 
 
 class TestMechanismDriver(api.MechanismDriver):
@@ -24,7 +24,7 @@ class TestMechanismDriver(api.MechanismDriver):
         pass
 
     def _check_network_context(self, context, original_expected):
-        assert(isinstance(context, driver_context.NetworkContext))
+        assert(isinstance(context, api.NetworkContext))
         assert(isinstance(context.current, dict))
         assert(context.current['id'] is not None)
         if original_expected:
@@ -53,7 +53,7 @@ class TestMechanismDriver(api.MechanismDriver):
         self._check_network_context(context, False)
 
     def _check_subnet_context(self, context, original_expected):
-        assert(isinstance(context, driver_context.SubnetContext))
+        assert(isinstance(context, api.SubnetContext))
         assert(isinstance(context.current, dict))
         assert(context.current['id'] is not None)
         if original_expected:
@@ -81,7 +81,7 @@ class TestMechanismDriver(api.MechanismDriver):
         self._check_subnet_context(context, False)
 
     def _check_port_context(self, context, original_expected):
-        assert(isinstance(context, driver_context.PortContext))
+        assert(isinstance(context, api.PortContext))
         assert(isinstance(context.current, dict))
         assert(context.current['id'] is not None)
         if original_expected:
@@ -90,7 +90,7 @@ class TestMechanismDriver(api.MechanismDriver):
         else:
             assert(not context.original)
         network_context = context.network
-        assert(isinstance(network_context, driver_context.NetworkContext))
+        assert(isinstance(network_context, api.NetworkContext))
         self._check_network_context(network_context, False)
 
     def create_port_precommit(self, context):
@@ -110,3 +110,19 @@ class TestMechanismDriver(api.MechanismDriver):
 
     def delete_port_postcommit(self, context):
         self._check_port_context(context, False)
+
+    def bind_port(self, context):
+        self._check_port_context(context, False)
+        host = context.current.get(portbindings.HOST_ID, None)
+        segment = context.network.network_segments[0][api.ID]
+        if host == "host-ovs-no_filter":
+            context.set_binding(segment, portbindings.VIF_TYPE_OVS, False)
+        elif host == "host-bridge-filter":
+            context.set_binding(segment, portbindings.VIF_TYPE_BRIDGE, True)
+
+    def validate_port_binding(self, context):
+        self._check_port_context(context, False)
+        return True
+
+    def unbind_port(self, context):
+        self._check_port_context(context, False)
diff --git a/neutron/tests/unit/ml2/test_mech_hyperv.py b/neutron/tests/unit/ml2/test_mech_hyperv.py
new file mode 100644 (file)
index 0000000..60ac1a6
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.plugins.ml2.drivers import mech_hyperv
+from neutron.tests.unit.ml2 import _test_mech_agent as base
+
+
+class HypervMechanismBaseTestCase(base.AgentMechanismBaseTestCase):
+    VIF_TYPE = portbindings.VIF_TYPE_HYPERV
+    CAP_PORT_FILTER = False
+    AGENT_TYPE = constants.AGENT_TYPE_HYPERV
+
+    GOOD_MAPPINGS = {'fake_physical_network': 'fake_vswitch'}
+    GOOD_CONFIGS = {'vswitch_mappings': GOOD_MAPPINGS}
+
+    BAD_MAPPINGS = {'wrong_physical_network': 'wrong_vswitch'}
+    BAD_CONFIGS = {'vswitch_mappings': BAD_MAPPINGS}
+
+    AGENTS = [{'alive': True,
+               'configurations': GOOD_CONFIGS}]
+    AGENTS_DEAD = [{'alive': False,
+                    'configurations': GOOD_CONFIGS}]
+    AGENTS_BAD = [{'alive': False,
+                   'configurations': GOOD_CONFIGS},
+                  {'alive': True,
+                   'configurations': BAD_CONFIGS}]
+
+    def setUp(self):
+        super(HypervMechanismBaseTestCase, self).setUp()
+        self.driver = mech_hyperv.HypervMechanismDriver()
+        self.driver.initialize()
+
+
+class HypervMechanismGenericTestCase(HypervMechanismBaseTestCase,
+                                     base.AgentMechanismGenericTestCase):
+    pass
+
+
+class HypervMechanismLocalTestCase(HypervMechanismBaseTestCase,
+                                   base.AgentMechanismLocalTestCase):
+    pass
+
+
+class HypervMechanismFlatTestCase(HypervMechanismBaseTestCase,
+                                  base.AgentMechanismFlatTestCase):
+    pass
+
+
+class HypervMechanismVlanTestCase(HypervMechanismBaseTestCase,
+                                  base.AgentMechanismVlanTestCase):
+    pass
diff --git a/neutron/tests/unit/ml2/test_mech_linuxbridge.py b/neutron/tests/unit/ml2/test_mech_linuxbridge.py
new file mode 100644 (file)
index 0000000..6ccc5b0
--- /dev/null
@@ -0,0 +1,65 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.plugins.ml2.drivers import mech_linuxbridge
+from neutron.tests.unit.ml2 import _test_mech_agent as base
+
+
+class LinuxbridgeMechanismBaseTestCase(base.AgentMechanismBaseTestCase):
+    VIF_TYPE = portbindings.VIF_TYPE_BRIDGE
+    CAP_PORT_FILTER = True
+    AGENT_TYPE = constants.AGENT_TYPE_LINUXBRIDGE
+
+    GOOD_MAPPINGS = {'fake_physical_network': 'fake_interface'}
+    GOOD_CONFIGS = {'interface_mappings': GOOD_MAPPINGS}
+
+    BAD_MAPPINGS = {'wrong_physical_network': 'wrong_interface'}
+    BAD_CONFIGS = {'interface_mappings': BAD_MAPPINGS}
+
+    AGENTS = [{'alive': True,
+               'configurations': GOOD_CONFIGS}]
+    AGENTS_DEAD = [{'alive': False,
+                    'configurations': GOOD_CONFIGS}]
+    AGENTS_BAD = [{'alive': False,
+                   'configurations': GOOD_CONFIGS},
+                  {'alive': True,
+                   'configurations': BAD_CONFIGS}]
+
+    def setUp(self):
+        super(LinuxbridgeMechanismBaseTestCase, self).setUp()
+        self.driver = mech_linuxbridge.LinuxbridgeMechanismDriver()
+        self.driver.initialize()
+
+
+class LinuxbridgeMechanismGenericTestCase(LinuxbridgeMechanismBaseTestCase,
+                                          base.AgentMechanismGenericTestCase):
+    pass
+
+
+class LinuxbridgeMechanismLocalTestCase(LinuxbridgeMechanismBaseTestCase,
+                                        base.AgentMechanismLocalTestCase):
+    pass
+
+
+class LinuxbridgeMechanismFlatTestCase(LinuxbridgeMechanismBaseTestCase,
+                                       base.AgentMechanismFlatTestCase):
+    pass
+
+
+class LinuxbridgeMechanismVlanTestCase(LinuxbridgeMechanismBaseTestCase,
+                                       base.AgentMechanismVlanTestCase):
+    pass
diff --git a/neutron/tests/unit/ml2/test_mech_openvswitch.py b/neutron/tests/unit/ml2/test_mech_openvswitch.py
new file mode 100644 (file)
index 0000000..b1af1b7
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.common import constants
+from neutron.extensions import portbindings
+from neutron.plugins.ml2.drivers import mech_openvswitch
+from neutron.tests.unit.ml2 import _test_mech_agent as base
+
+
+class OpenvswitchMechanismBaseTestCase(base.AgentMechanismBaseTestCase):
+    VIF_TYPE = portbindings.VIF_TYPE_OVS
+    CAP_PORT_FILTER = True
+    AGENT_TYPE = constants.AGENT_TYPE_OVS
+
+    GOOD_MAPPINGS = {'fake_physical_network': 'fake_bridge'}
+    GOOD_TUNNEL_TYPES = ['gre', 'vxlan']
+    GOOD_CONFIGS = {'bridge_mappings': GOOD_MAPPINGS,
+                    'tunnel_types': GOOD_TUNNEL_TYPES}
+
+    BAD_MAPPINGS = {'wrong_physical_network': 'wrong_bridge'}
+    BAD_TUNNEL_TYPES = ['bad_tunnel_type']
+    BAD_CONFIGS = {'bridge_mappings': BAD_MAPPINGS,
+                   'tunnel_types': BAD_TUNNEL_TYPES}
+
+    AGENTS = [{'alive': True,
+               'configurations': GOOD_CONFIGS}]
+    AGENTS_DEAD = [{'alive': False,
+                    'configurations': GOOD_CONFIGS}]
+    AGENTS_BAD = [{'alive': False,
+                   'configurations': GOOD_CONFIGS},
+                  {'alive': True,
+                   'configurations': BAD_CONFIGS}]
+
+    def setUp(self):
+        super(OpenvswitchMechanismBaseTestCase, self).setUp()
+        self.driver = mech_openvswitch.OpenvswitchMechanismDriver()
+        self.driver.initialize()
+
+
+class OpenvswitchMechanismGenericTestCase(OpenvswitchMechanismBaseTestCase,
+                                          base.AgentMechanismGenericTestCase):
+    pass
+
+
+class OpenvswitchMechanismLocalTestCase(OpenvswitchMechanismBaseTestCase,
+                                        base.AgentMechanismLocalTestCase):
+    pass
+
+
+class OpenvswitchMechanismFlatTestCase(OpenvswitchMechanismBaseTestCase,
+                                       base.AgentMechanismFlatTestCase):
+    pass
+
+
+class OpenvswitchMechanismVlanTestCase(OpenvswitchMechanismBaseTestCase,
+                                       base.AgentMechanismVlanTestCase):
+    pass
+
+
+class OpenvswitchMechanismGreTestCase(OpenvswitchMechanismBaseTestCase,
+                                      base.AgentMechanismGreTestCase):
+    pass
index d2fefe0eb9cf0f8ace4eac07e5435f451af1a803..202230d416c12838cecb38987c275e00fe072e76 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron.extensions import portbindings
 from neutron.plugins.ml2 import config as config
 from neutron.tests.unit import _test_extension_portbindings as test_bindings
 from neutron.tests.unit import test_db_plugin as test_plugin
 from neutron.tests.unit import test_extension_ext_gw_mode
+from neutron.tests.unit import test_security_groups_rpc as test_sg_rpc
 
 
 PLUGIN_NAME = 'neutron.plugins.ml2.plugin.Ml2Plugin'
@@ -61,10 +63,22 @@ class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase):
             self.assertEqual(self.port_create_status, 'DOWN')
 
 
-# TODO(rkukura) add TestMl2PortBinding
+class TestMl2PortBinding(Ml2PluginV2TestCase,
+                         test_bindings.PortBindingsTestCase):
+    # Test case does not set binding:host_id, so ml2 does not attempt
+    # to bind port
+    VIF_TYPE = portbindings.VIF_TYPE_UNBOUND
+    HAS_PORT_FILTER = False
+    FIREWALL_DRIVER = test_sg_rpc.FIREWALL_HYBRID_DRIVER
 
+    def setUp(self, firewall_driver=None):
+        test_sg_rpc.set_firewall_driver(self.FIREWALL_DRIVER)
+        super(TestMl2PortBinding, self).setUp()
 
-# TODO(rkukura) add TestMl2PortBindingNoSG
+
+class TestMl2PortBindingNoSG(TestMl2PortBinding):
+    HAS_PORT_FILTER = False
+    FIREWALL_DRIVER = test_sg_rpc.FIREWALL_NOOP_DRIVER
 
 
 class TestMl2PortBindingHost(Ml2PluginV2TestCase,
diff --git a/neutron/tests/unit/ml2/test_port_binding.py b/neutron/tests/unit/ml2/test_port_binding.py
new file mode 100644 (file)
index 0000000..8b02236
--- /dev/null
@@ -0,0 +1,78 @@
+# Copyright (c) 2013 OpenStack Foundation
+# 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.extensions import portbindings
+from neutron import manager
+from neutron.plugins.ml2 import config as config
+from neutron.tests.unit import test_db_plugin as test_plugin
+
+
+PLUGIN_NAME = 'neutron.plugins.ml2.plugin.Ml2Plugin'
+
+
+class PortBindingTestCase(test_plugin.NeutronDbPluginV2TestCase):
+
+    _plugin_name = PLUGIN_NAME
+
+    def setUp(self):
+        # Enable the test mechanism driver to ensure that
+        # we can successfully call through to all mechanism
+        # driver apis.
+        config.cfg.CONF.set_override('mechanism_drivers',
+                                     ['logger', 'test'],
+                                     'ml2')
+        self.addCleanup(config.cfg.CONF.reset)
+        super(PortBindingTestCase, self).setUp(PLUGIN_NAME)
+        self.port_create_status = 'DOWN'
+        self.plugin = manager.NeutronManager.get_plugin()
+
+    def _check_response(self, port, vif_type, has_port_filter):
+        self.assertEqual(port['binding:vif_type'], vif_type)
+        port_cap = port[portbindings.CAPABILITIES]
+        self.assertEqual(port_cap[portbindings.CAP_PORT_FILTER],
+                         has_port_filter)
+
+    def _test_port_binding(self, host, vif_type, has_port_filter, bound):
+        host_arg = {portbindings.HOST_ID: host}
+        with self.port(name='name', arg_list=(portbindings.HOST_ID,),
+                       **host_arg) as port:
+            self._check_response(port['port'], vif_type, has_port_filter)
+            port_id = port['port']['id']
+            details = self.plugin.callbacks.get_device_details(
+                None, agent_id="theAgentId", device=port_id)
+            if bound:
+                self.assertEqual(details['network_type'], 'local')
+            else:
+                self.assertNotIn('network_type', details)
+
+    def test_unbound(self):
+        self._test_port_binding("",
+                                portbindings.VIF_TYPE_UNBOUND,
+                                False, False)
+
+    def test_binding_failed(self):
+        self._test_port_binding("host-fail",
+                                portbindings.VIF_TYPE_BINDING_FAILED,
+                                False, False)
+
+    def test_binding_no_filter(self):
+        self._test_port_binding("host-ovs-no_filter",
+                                portbindings.VIF_TYPE_OVS,
+                                False, True)
+
+    def test_binding_filter(self):
+        self._test_port_binding("host-bridge-filter",
+                                portbindings.VIF_TYPE_BRIDGE,
+                                True, True)
index e5f73eb5c65534e02beb574632251d0fc75ae441..e7d97d0f7fdf0358bce4927d77ad402bfd6c8181 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -117,6 +117,9 @@ neutron.ml2.type_drivers =
 neutron.ml2.mechanism_drivers =
     logger = neutron.tests.unit.ml2.drivers.mechanism_logger:LoggerMechanismDriver
     test = neutron.tests.unit.ml2.drivers.mechanism_test:TestMechanismDriver
+    linuxbridge = neutron.plugins.ml2.drivers.mech_linuxbridge:LinuxbridgeMechanismDriver
+    openvswitch = neutron.plugins.ml2.drivers.mech_openvswitch:OpenvswitchMechanismDriver
+    hyperv = neutron.plugins.ml2.drivers.mech_hyperv:HypervMechanismDriver
     ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver
     arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver