diff --git a/etc/neutron/plugins/ml2/ml2_conf_sriov.ini b/etc/neutron/plugins/ml2/ml2_conf_sriov.ini new file mode 100644 index 0000000000..09504af2d8 --- /dev/null +++ b/etc/neutron/plugins/ml2/ml2_conf_sriov.ini @@ -0,0 +1,12 @@ +# Defines configuration options for SRIOV NIC Switch MechanismDriver + +[ml2_sriov] +# (ListOpt) Comma-separated list of +# supported Vendor PCI Devices, in format vendor_id:product_id +# +# supported_vendor_pci_devs = 15b3:1004 +# Example: supported_vendor_pci_devs = 15b3:1004, 8086:10c9 +# +# (BoolOpt) Requires SRIOV neutron agent for port binding +# agent_required = True + diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 6c321139e3..e1c61a8bc7 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -87,6 +87,7 @@ AGENT_TYPE_MLNX = 'Mellanox plugin agent' AGENT_TYPE_METERING = 'Metering agent' AGENT_TYPE_METADATA = 'Metadata agent' AGENT_TYPE_SDNVE = 'IBM SDN-VE agent' +AGENT_TYPE_NIC_SWITCH = 'NIC Switch agent' L2_AGENT_TOPIC = 'N/A' PAGINATION_INFINITE = 'infinite' diff --git a/neutron/extensions/portbindings.py b/neutron/extensions/portbindings.py index 7e5c76dd3f..97574a1bff 100644 --- a/neutron/extensions/portbindings.py +++ b/neutron/extensions/portbindings.py @@ -45,6 +45,7 @@ PROFILE = 'binding:profile' # strategy for OVS should be used CAP_PORT_FILTER = 'port_filter' OVS_HYBRID_PLUG = 'ovs_hybrid_plug' +VIF_DETAILS_VLAN = 'vlan' VIF_TYPE_UNBOUND = 'unbound' VIF_TYPE_BINDING_FAILED = 'binding_failed' @@ -58,11 +59,13 @@ VIF_TYPE_HYPERV = 'hyperv' VIF_TYPE_MIDONET = 'midonet' VIF_TYPE_MLNX_DIRECT = 'mlnx_direct' VIF_TYPE_MLNX_HOSTDEV = 'hostdev' +VIF_TYPE_HW_VEB = 'hw_veb' VIF_TYPE_OTHER = 'other' VIF_TYPES = [VIF_TYPE_UNBOUND, VIF_TYPE_BINDING_FAILED, VIF_TYPE_OVS, VIF_TYPE_IVS, VIF_TYPE_BRIDGE, VIF_TYPE_802_QBG, VIF_TYPE_802_QBH, VIF_TYPE_HYPERV, VIF_TYPE_MIDONET, - VIF_TYPE_MLNX_DIRECT, VIF_TYPE_MLNX_HOSTDEV, VIF_TYPE_OTHER] + VIF_TYPE_MLNX_DIRECT, VIF_TYPE_MLNX_HOSTDEV, VIF_TYPE_HW_VEB, + VIF_TYPE_OTHER] VNIC_NORMAL = 'normal' VNIC_DIRECT = 'direct' diff --git a/neutron/plugins/ml2/drivers/mech_sriov/__init__.py b/neutron/plugins/ml2/drivers/mech_sriov/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/plugins/ml2/drivers/mech_sriov/mech_driver.py b/neutron/plugins/ml2/drivers/mech_sriov/mech_driver.py new file mode 100644 index 0000000000..3db3b890fd --- /dev/null +++ b/neutron/plugins/ml2/drivers/mech_sriov/mech_driver.py @@ -0,0 +1,184 @@ +# Copyright 2014 Mellanox Technologies, Ltd +# +# 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 oslo.config import cfg + +from neutron.common import constants +from neutron.extensions import portbindings +from neutron.openstack.common import log +from neutron.plugins.common import constants as p_const +from neutron.plugins.ml2 import driver_api as api + + +LOG = log.getLogger(__name__) + +sriov_opts = [ + cfg.ListOpt('supported_pci_vendor_devs', + default=['15b3:1004', '8086:10c9'], + help=_("Supported PCI vendor devices, defined by " + "vendor_id:product_id according to the PCI ID " + "Repository. Default enables support for Intel " + "and Mellanox SR-IOV capable NICs")), + cfg.BoolOpt('agent_required', + default=False, + help=_("SRIOV neutron agent is required for port binding")), + +] + +cfg.CONF.register_opts(sriov_opts, "ml2_sriov") + + +class SriovNicSwitchMechanismDriver(api.MechanismDriver): + """Mechanism Driver for SR-IOV capable NIC based switching. + + The SriovNicSwitchMechanismDriver integrates the ml2 plugin with the + sriovNicSwitch L2 agent depending on configuration option. + Port binding with this driver may require the sriovNicSwitch 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. + L2 agent is not essential for port binding; port binding is handled by + VIF Driver via libvirt domain XML. + L2 Agent presents in order to manage port update events. + If vendor NIC does not support updates, setting agent_required = False + will allow to use Mechanism Driver without L2 agent. + + """ + + def __init__(self, + agent_type=constants.AGENT_TYPE_NIC_SWITCH, + vif_type=portbindings.VIF_TYPE_HW_VEB, + vif_details={portbindings.CAP_PORT_FILTER: False}, + supported_vnic_types=[portbindings.VNIC_DIRECT, + portbindings.VNIC_MACVTAP], + supported_pci_vendor_info=None): + """Initialize base class for SriovNicSwitch L2 agent type. + + :param agent_type: Constant identifying agent type in agents_db + :param vif_type: Value for binding:vif_type when bound + :param vif_details: Dictionary with details for VIF driver when bound + :param supported_vnic_types: The binding:vnic_type values we can bind + :param supported_pci_vendor_info: The pci_vendor_info values to bind + """ + self.agent_type = agent_type + self.supported_vnic_types = supported_vnic_types + self.vif_type = vif_type + self.vif_details = vif_details + + def initialize(self): + try: + self.pci_vendor_info = self._parse_pci_vendor_config( + cfg.CONF.ml2_sriov.supported_pci_vendor_devs) + self.agent_required = cfg.CONF.ml2_sriov.agent_required + except ValueError: + LOG.exception(_("Failed to parse supported PCI vendor devices")) + raise cfg.Error(_("Parsing supported pci_vendor_devs failed")) + + 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']}) + vnic_type = context.current.get(portbindings.VNIC_TYPE, + portbindings.VNIC_NORMAL) + if vnic_type not in self.supported_vnic_types: + LOG.debug("Refusing to bind due to unsupported vnic_type: %s", + vnic_type) + return + + if not self._check_supported_pci_vendor_device(context): + LOG.debug("Refusing to bind due to unsupported pci_vendor device") + return + + if self.agent_required: + for agent in context.host_agents(self.agent_type): + LOG.debug("Checking agent: %s", agent) + if agent['alive']: + if self.try_to_bind(context, agent): + return + else: + LOG.warning(_("Attempting to bind with dead agent: %s"), + agent) + else: + self.try_to_bind(context) + + def try_to_bind(self, context, agent=None): + for segment in context.network.network_segments: + if self.check_segment(segment, agent): + context.set_binding(segment[api.ID], + self.vif_type, + self.get_vif_details(context, segment), + constants.PORT_STATUS_ACTIVE) + LOG.debug("Bound using segment: %s", segment) + return True + return False + + def check_segment(self, segment, agent=None): + """Check if segment can be bound. + + :param segment: segment dictionary describing segment to bind + :param agent: agents_db entry describing agent to bind or None + :returns: True if segment can be bound for agent + """ + network_type = segment[api.NETWORK_TYPE] + if network_type == p_const.TYPE_VLAN: + if agent: + mappings = agent['configurations'].get('device_mappings', {}) + LOG.debug("Checking segment: %(segment)s " + "for mappings: %(mappings)s ", + {'segment': segment, 'mappings': mappings}) + return segment[api.PHYSICAL_NETWORK] in mappings + return True + return False + + def _check_supported_pci_vendor_device(self, context): + if self.pci_vendor_info: + profile = context.current.get(portbindings.PROFILE, {}) + if not profile: + LOG.debug("Missing profile in port binding") + return False + pci_vendor_info = profile.get('pci_vendor_info') + if not pci_vendor_info: + LOG.debug("Missing pci vendor info in profile") + return False + if pci_vendor_info not in self.pci_vendor_info: + LOG.debug("Unsupported pci_vendor %s", pci_vendor_info) + return False + return True + return False + + def get_vif_details(self, context, segment): + if segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN: + vlan_id = str(segment[api.SEGMENTATION_ID]) + self.vif_details[portbindings.VIF_DETAILS_VLAN] = vlan_id + return self.vif_details + + def _parse_pci_vendor_config(self, pci_vendor_list): + parsed_list = [] + for elem in pci_vendor_list: + elem = elem.strip() + if not elem: + continue + split_result = elem.split(':') + if len(split_result) != 2: + raise ValueError(_("Invalid pci_vendor_info: '%s'") % elem) + vendor_id = split_result[0].strip() + if not vendor_id: + raise ValueError(_("Missing vendor_id in: '%s'") % elem) + product_id = split_result[1].strip() + if not product_id: + raise ValueError(_("Missing product_id in: '%s'") % elem) + parsed_list.append(elem) + return parsed_list diff --git a/neutron/plugins/ml2/rpc.py b/neutron/plugins/ml2/rpc.py index 0ee8c00999..a67d0186b0 100644 --- a/neutron/plugins/ml2/rpc.py +++ b/neutron/plugins/ml2/rpc.py @@ -125,7 +125,8 @@ class RpcCallbacks(n_rpc.RpcCallback, 'segmentation_id': segment[api.SEGMENTATION_ID], 'physical_network': segment[api.PHYSICAL_NETWORK], 'fixed_ips': port['fixed_ips'], - 'device_owner': port['device_owner']} + 'device_owner': port['device_owner'], + 'profile': port[portbindings.PROFILE]} LOG.debug(_("Returning: %s"), entry) return entry diff --git a/neutron/tests/unit/ml2/drivers/mech_sriov/__init__.py b/neutron/tests/unit/ml2/drivers/mech_sriov/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/tests/unit/ml2/drivers/mech_sriov/test_mech_sriov_nic_switch.py b/neutron/tests/unit/ml2/drivers/mech_sriov/test_mech_sriov_nic_switch.py new file mode 100644 index 0000000000..e854f06840 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/mech_sriov/test_mech_sriov_nic_switch.py @@ -0,0 +1,236 @@ +# Copyright 2014 Mellanox Technologies, Ltd +# +# 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. + +import mock +from oslo.config import cfg + +from neutron.common import constants +from neutron.extensions import portbindings +from neutron.plugins.common import constants as p_const +from neutron.plugins.ml2 import config # noqa +from neutron.plugins.ml2 import driver_api as api +from neutron.plugins.ml2.drivers.mech_sriov import mech_driver +from neutron.tests.unit.ml2 import _test_mech_agent as base + +MELLANOX_CONNECTX3_PCI_INFO = '15b3:1004' +DEFAULT_PCI_INFO = ['15b3:1004', '8086:10c9'] + + +class TestFakePortContext(base.FakePortContext): + def __init__(self, agent_type, agents, segments, + vnic_type=portbindings.VNIC_NORMAL, + profile={'pci_vendor_info': + MELLANOX_CONNECTX3_PCI_INFO}): + super(TestFakePortContext, self).__init__(agent_type, + agents, + segments, + vnic_type) + self._bound_profile = profile + + @property + def current(self): + return {'id': base.PORT_ID, + 'binding:vnic_type': self._bound_vnic_type, + 'binding:profile': self._bound_profile} + + def set_binding(self, segment_id, vif_type, vif_details, state): + self._bound_segment_id = segment_id + self._bound_vif_type = vif_type + self._bound_vif_details = vif_details + self._bound_state = state + + +class SriovNicSwitchMechanismBaseTestCase(base.AgentMechanismBaseTestCase): + VIF_TYPE = portbindings.VIF_TYPE_HW_VEB + CAP_PORT_FILTER = False + AGENT_TYPE = constants.AGENT_TYPE_NIC_SWITCH + VLAN_SEGMENTS = base.AgentMechanismVlanTestCase.VLAN_SEGMENTS + + GOOD_MAPPINGS = {'fake_physical_network': 'fake_device'} + GOOD_CONFIGS = {'device_mappings': GOOD_MAPPINGS} + + BAD_MAPPINGS = {'wrong_physical_network': 'wrong_device'} + BAD_CONFIGS = {'device_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): + cfg.CONF.set_override('supported_pci_vendor_devs', + DEFAULT_PCI_INFO, + 'ml2_sriov') + cfg.CONF.set_override('agent_required', True, 'ml2_sriov') + super(SriovNicSwitchMechanismBaseTestCase, self).setUp() + self.driver = mech_driver.SriovNicSwitchMechanismDriver() + self.driver.initialize() + + def test_check_segment(self): + """Validate the check_segment call.""" + segment = {'api.NETWORK_TYPE': ""} + segment[api.NETWORK_TYPE] = p_const.TYPE_VLAN + self.assertTrue(self.driver.check_segment(segment)) + # Validate a network type not currently supported + segment[api.NETWORK_TYPE] = p_const.TYPE_GRE + self.assertFalse(self.driver.check_segment(segment)) + + +class SriovSwitchMechGenericTestCase(SriovNicSwitchMechanismBaseTestCase, + base.AgentMechanismGenericTestCase): + pass + + +class SriovMechVlanTestCase(SriovNicSwitchMechanismBaseTestCase, + base.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 = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS, + self.VLAN_SEGMENTS, + portbindings.VNIC_DIRECT) + self.driver.bind_port(context) + self._check_bound(context, self.VLAN_SEGMENTS[1]) + + def test_type_vlan_bad(self): + context = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS_BAD, + self.VLAN_SEGMENTS, + portbindings.VNIC_DIRECT) + self.driver.bind_port(context) + self._check_unbound(context) + + +class SriovSwitchMechVnicTypeTestCase(SriovNicSwitchMechanismBaseTestCase): + def _check_vif_type_for_vnic_type(self, vnic_type, + expected_vif_type): + context = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS, + self.VLAN_SEGMENTS, + vnic_type) + self.driver.bind_port(context) + self.assertEqual(expected_vif_type, context._bound_vif_type) + vlan = int(context._bound_vif_details[portbindings.VIF_DETAILS_VLAN]) + self.assertEqual(1234, vlan) + + def test_vnic_type_direct(self): + self._check_vif_type_for_vnic_type(portbindings.VNIC_DIRECT, + portbindings.VIF_TYPE_HW_VEB) + + def test_vnic_type_macvtap(self): + self._check_vif_type_for_vnic_type(portbindings.VNIC_MACVTAP, + portbindings.VIF_TYPE_HW_VEB) + + +class SriovSwitchMechProfileTestCase(SriovNicSwitchMechanismBaseTestCase): + def _check_vif_for_pci_info(self, pci_vendor_info, expected_vif_type): + context = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS, + self.VLAN_SEGMENTS, + portbindings.VNIC_DIRECT, + {'pci_vendor_info': pci_vendor_info}) + self.driver.bind_port(context) + self.assertEqual(expected_vif_type, context._bound_vif_type) + + def test_profile_supported_pci_info(self): + self._check_vif_for_pci_info(MELLANOX_CONNECTX3_PCI_INFO, + portbindings.VIF_TYPE_HW_VEB) + + def test_profile_unsupported_pci_info(self): + with mock.patch('neutron.plugins.ml2.drivers.mech_sriov.' + 'mech_driver.LOG') as log_mock: + self._check_vif_for_pci_info('xxxx:yyyy', None) + log_mock.debug.assert_called_with('Refusing to bind due to ' + 'unsupported pci_vendor device') + + +class SriovSwitchMechProfileFailTestCase(SriovNicSwitchMechanismBaseTestCase): + def _check_for_pci_vendor_info(self, pci_vendor_info): + context = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS, + self.VLAN_SEGMENTS, + portbindings.VNIC_DIRECT, + pci_vendor_info) + self.driver._check_supported_pci_vendor_device(context) + + def test_profile_missing_profile(self): + with mock.patch('neutron.plugins.ml2.drivers.mech_sriov.' + 'mech_driver.LOG') as log_mock: + self._check_for_pci_vendor_info({}) + log_mock.debug.assert_called_with("Missing profile in port" + " binding") + + def test_profile_missing_pci_vendor_info(self): + with mock.patch('neutron.plugins.ml2.drivers.mech_sriov.' + 'mech_driver.LOG') as log_mock: + self._check_for_pci_vendor_info({'aa': 'bb'}) + log_mock.debug.assert_called_with("Missing pci vendor" + " info in profile") + + +class SriovSwitchMechVifDetailsTestCase(SriovNicSwitchMechanismBaseTestCase): + def test_vif_details_contains_vlan_id(self): + VLAN_SEGMENTS = [{api.ID: 'vlan_segment_id', + api.NETWORK_TYPE: 'vlan', + api.PHYSICAL_NETWORK: 'fake_physical_network', + api.SEGMENTATION_ID: 1234}] + + context = TestFakePortContext(self.AGENT_TYPE, + self.AGENTS, + VLAN_SEGMENTS, + portbindings.VNIC_DIRECT) + + self.driver.bind_port(context) + vif_details = context._bound_vif_details + self.assertIsNotNone(vif_details) + vlan_id = int(vif_details.get(portbindings.VIF_DETAILS_VLAN)) + self.assertEqual(1234, vlan_id) + + +class SriovSwitchMechConfigTestCase(SriovNicSwitchMechanismBaseTestCase): + def _set_config(self, pci_devs=['aa:bb']): + cfg.CONF.set_override('mechanism_drivers', + ['logger', 'sriovnicswitch'], 'ml2') + cfg.CONF.set_override('supported_pci_vendor_devs', pci_devs, + 'ml2_sriov') + + def test_pci_vendor_config_single_entry(self): + self._set_config() + self.driver.initialize() + self.assertEqual(['aa:bb'], self.driver.pci_vendor_info) + + def test_pci_vendor_config_multiple_entry(self): + self._set_config(['x:y', 'a:b']) + self.driver.initialize() + self.assertEqual(['x:y', 'a:b'], self.driver.pci_vendor_info) + + def test_pci_vendor_config_default_entry(self): + self.driver.initialize() + self.assertEqual(DEFAULT_PCI_INFO, + self.driver.pci_vendor_info) + + def test_pci_vendor_config_wrong_entry(self): + self._set_config('wrong_entry') + self.assertRaises(cfg.Error, self.driver.initialize) diff --git a/setup.cfg b/setup.cfg index 94f7d9cd7b..df4d6cfb4d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ data_files = etc/neutron/plugins/ml2/ml2_conf_odl.ini etc/neutron/plugins/ml2/ml2_conf_ofa.ini etc/neutron/plugins/ml2/ml2_conf_fslsdn.ini + etc/neutron/plugins/ml2/ml2_conf_sriov.ini etc/neutron/plugins/mlnx = etc/neutron/plugins/mlnx/mlnx_conf.ini etc/neutron/plugins/nec = etc/neutron/plugins/nec/nec.ini etc/neutron/plugins/nuage = etc/neutron/plugins/nuage/nuage_plugin.ini @@ -167,6 +168,7 @@ neutron.ml2.mechanism_drivers = mlnx = neutron.plugins.ml2.drivers.mlnx.mech_mlnx:MlnxMechanismDriver brocade = neutron.plugins.ml2.drivers.brocade.mechanism_brocade:BrocadeMechanism fslsdn = neutron.plugins.ml2.drivers.mechanism_fslsdn:FslsdnMechanismDriver + sriovnicswitch = neutron.plugins.ml2.drivers.mech_sriov.mech_driver:SriovNicSwitchMechanismDriver neutron.openstack.common.cache.backends = memory = neutron.openstack.common.cache._backends.memory:MemoryBackend # These are for backwards compat with Icehouse notification_driver configuration values