From 8bc02a7fbeaf2e0c8bccbaad52f41d026c1bbf40 Mon Sep 17 00:00:00 2001 From: Bob Kukura Date: Mon, 12 Aug 2013 21:58:36 -0400 Subject: [PATCH] Implement ML2 port binding 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 --- neutron/common/constants.py | 2 + neutron/db/dhcp_rpc_base.py | 2 + neutron/db/l3_rpc_base.py | 22 ++ .../versions/32a65f71af51_ml2_portbinding.py | 70 ++++++ .../agent/linuxbridge_neutron_agent.py | 2 +- neutron/plugins/ml2/db.py | 20 +- neutron/plugins/ml2/driver_api.py | 65 ++++++ neutron/plugins/ml2/driver_context.py | 46 ++-- neutron/plugins/ml2/drivers/mech_agent.py | 105 +++++++++ neutron/plugins/ml2/drivers/mech_hyperv.py | 51 +++++ .../plugins/ml2/drivers/mech_linuxbridge.py | 52 +++++ .../plugins/ml2/drivers/mech_openvswitch.py | 57 +++++ neutron/plugins/ml2/managers.py | 100 +++++++-- neutron/plugins/ml2/models.py | 32 +++ neutron/plugins/ml2/plugin.py | 178 ++++++++------- neutron/plugins/ml2/rpc.py | 31 ++- .../openvswitch/agent/ovs_neutron_agent.py | 4 +- neutron/tests/unit/ml2/_test_mech_agent.py | 207 ++++++++++++++++++ .../unit/ml2/drivers/mechanism_logger.py | 10 + .../tests/unit/ml2/drivers/mechanism_test.py | 26 ++- neutron/tests/unit/ml2/test_mech_hyperv.py | 65 ++++++ .../tests/unit/ml2/test_mech_linuxbridge.py | 65 ++++++ .../tests/unit/ml2/test_mech_openvswitch.py | 74 +++++++ neutron/tests/unit/ml2/test_ml2_plugin.py | 18 +- neutron/tests/unit/ml2/test_port_binding.py | 78 +++++++ setup.cfg | 3 + 26 files changed, 1269 insertions(+), 116 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/32a65f71af51_ml2_portbinding.py create mode 100644 neutron/plugins/ml2/drivers/mech_agent.py create mode 100644 neutron/plugins/ml2/drivers/mech_hyperv.py create mode 100644 neutron/plugins/ml2/drivers/mech_linuxbridge.py create mode 100644 neutron/plugins/ml2/drivers/mech_openvswitch.py create mode 100644 neutron/tests/unit/ml2/_test_mech_agent.py create mode 100644 neutron/tests/unit/ml2/test_mech_hyperv.py create mode 100644 neutron/tests/unit/ml2/test_mech_linuxbridge.py create mode 100644 neutron/tests/unit/ml2/test_mech_openvswitch.py create mode 100644 neutron/tests/unit/ml2/test_port_binding.py diff --git a/neutron/common/constants.py b/neutron/common/constants.py index a41fe8553..f8ff46e4d 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -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' diff --git a/neutron/db/dhcp_rpc_base.py b/neutron/db/dhcp_rpc_base.py index 25a2876cd..ee6c3db1a 100644 --- a/neutron/db/dhcp_rpc_base.py +++ b/neutron/db/dhcp_rpc_base.py @@ -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() diff --git a/neutron/db/l3_rpc_base.py b/neutron/db/l3_rpc_base.py index 1cad6c94c..afd64afe7 100644 --- a/neutron/db/l3_rpc_base.py +++ b/neutron/db/l3_rpc_base.py @@ -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 index 000000000..6256186f4 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/32a65f71af51_ml2_portbinding.py @@ -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') diff --git a/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py b/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py index c71f6acdb..b9e4d42c1 100755 --- a/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py +++ b/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py @@ -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} diff --git a/neutron/plugins/ml2/db.py b/neutron/plugins/ml2/db.py index 48c3b678e..c987d7860 100644 --- a/neutron/plugins/ml2/db.py +++ b/neutron/plugins/ml2/db.py @@ -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.""" diff --git a/neutron/plugins/ml2/driver_api.py b/neutron/plugins/ml2/driver_api.py index 546c23c94..e8cb77b0b 100644 --- a/neutron/plugins/ml2/driver_api.py +++ b/neutron/plugins/ml2/driver_api.py @@ -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 diff --git a/neutron/plugins/ml2/driver_context.py b/neutron/plugins/ml2/driver_context.py index 1f552d2d8..7468192f7 100644 --- a/neutron/plugins/ml2/driver_context.py +++ b/neutron/plugins/ml2/driver_context.py @@ -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 index 000000000..a89956f90 --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_agent.py @@ -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 index 000000000..a6c8d4225 --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_hyperv.py @@ -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 index 000000000..15ca7d66c --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_linuxbridge.py @@ -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 index 000000000..f98ddf288 --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_openvswitch.py @@ -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 diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index 54321cf72..e25d79d5a 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -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 diff --git a/neutron/plugins/ml2/models.py b/neutron/plugins/ml2/models.py index ba5650940..9b215be27 100644 --- a/neutron/plugins/ml2/models.py +++ b/neutron/plugins/ml2/models.py @@ -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')) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index c14243ddd..892685ef5 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -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) diff --git a/neutron/plugins/ml2/rpc.py b/neutron/plugins/ml2/rpc.py index 9b26bcfd9..d19995d74 100644 --- a/neutron/plugins/ml2/rpc.py +++ b/neutron/plugins/ml2/rpc.py @@ -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 diff --git a/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py index 36caea1e0..b89faa905 100644 --- a/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py @@ -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 index 000000000..83e772b7b --- /dev/null +++ b/neutron/tests/unit/ml2/_test_mech_agent.py @@ -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) diff --git a/neutron/tests/unit/ml2/drivers/mechanism_logger.py b/neutron/tests/unit/ml2/drivers/mechanism_logger.py index c900fdfe1..ca92dbc64 100644 --- a/neutron/tests/unit/ml2/drivers/mechanism_logger.py +++ b/neutron/tests/unit/ml2/drivers/mechanism_logger.py @@ -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) diff --git a/neutron/tests/unit/ml2/drivers/mechanism_test.py b/neutron/tests/unit/ml2/drivers/mechanism_test.py index bc1ce6f6d..24a405504 100644 --- a/neutron/tests/unit/ml2/drivers/mechanism_test.py +++ b/neutron/tests/unit/ml2/drivers/mechanism_test.py @@ -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 index 000000000..60ac1a620 --- /dev/null +++ b/neutron/tests/unit/ml2/test_mech_hyperv.py @@ -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 index 000000000..6ccc5b0b6 --- /dev/null +++ b/neutron/tests/unit/ml2/test_mech_linuxbridge.py @@ -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 index 000000000..b1af1b7fa --- /dev/null +++ b/neutron/tests/unit/ml2/test_mech_openvswitch.py @@ -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 diff --git a/neutron/tests/unit/ml2/test_ml2_plugin.py b/neutron/tests/unit/ml2/test_ml2_plugin.py index d2fefe0eb..202230d41 100644 --- a/neutron/tests/unit/ml2/test_ml2_plugin.py +++ b/neutron/tests/unit/ml2/test_ml2_plugin.py @@ -13,10 +13,12 @@ # 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 index 000000000..8b0223613 --- /dev/null +++ b/neutron/tests/unit/ml2/test_port_binding.py @@ -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) diff --git a/setup.cfg b/setup.cfg index e5f73eb5c..e7d97d0f7 100644 --- 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 -- 2.45.2