diff --git a/etc/neutron/plugins/ml2/ml2_conf_sriov.ini b/etc/neutron/plugins/ml2/ml2_conf_sriov.ini index 09504af2d8..438da7b8ff 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_sriov.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_sriov.ini @@ -1,12 +1,31 @@ # Defines configuration options for SRIOV NIC Switch MechanismDriver +# and Agent [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 +# supported_vendor_pci_devs = 15b3:1004, 8086:10c9 +# Example: supported_vendor_pci_devs = 15b3:1004 # -# (BoolOpt) Requires SRIOV neutron agent for port binding +# (BoolOpt) Requires running SRIOV neutron agent for port binding # agent_required = True +[sriov_nic] +# (ListOpt) Comma-separated list of : +# tuples mapping physical network names to the agent's node-specific +# physical network device interfaces of SR-IOV physical function to be used +# for VLAN networks. All physical networks listed in network_vlan_ranges on +# the server should have mappings to appropriate interfaces on each agent. +# +# physical_device_mappings = +# Example: physical_device_mappings = physnet1:eth1 +# +# (ListOpt) Comma-separated list of : +# tuples, mapping network_device to the agent's node-specific list of virtual +# functions that should not be used for virtual networking. +# vfs_to_exclude is a semicolon-separated list of virtual +# functions to exclude from network_device. The network_device in the +# mapping should appear in the physical_device_mappings list. +# exclude_devices = +# Example: exclude_list = eth1:0000:07:00.2; 0000:07:00.3 \ No newline at end of file diff --git a/neutron/plugins/sriovnicagent/__init__.py b/neutron/plugins/sriovnicagent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/plugins/sriovnicagent/common/__init__.py b/neutron/plugins/sriovnicagent/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/plugins/sriovnicagent/common/config.py b/neutron/plugins/sriovnicagent/common/config.py new file mode 100644 index 0000000000..9d8c6a41cd --- /dev/null +++ b/neutron/plugins/sriovnicagent/common/config.py @@ -0,0 +1,88 @@ +# 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.agent.common import config + + +def parse_exclude_devices(exclude_list): + """Parse Exclude devices list + + parses excluded device list in the form: + dev_name:pci_dev_1;pci_dev_2 + @param exclude list: list of string pairs in "key:value" format + the key part represents the network device name + the value part is a list of PCI slots separated by ";" + """ + exclude_mapping = {} + for dev_mapping in exclude_list: + try: + dev_name, exclude_devices = dev_mapping.split(":", 1) + except ValueError: + raise ValueError(_("Invalid mapping: '%s'") % dev_mapping) + dev_name = dev_name.strip() + if not dev_name: + raise ValueError(_("Missing key in mapping: '%s'") % dev_mapping) + if dev_name in exclude_mapping: + raise ValueError(_("Device %(dev_name)s in mapping: %(mapping)s " + "not unique") % {'dev_name': dev_name, + 'mapping': dev_mapping}) + exclude_devices_list = exclude_devices.split(";") + exclude_devices_set = set() + for dev in exclude_devices_list: + dev = dev.strip() + if dev: + exclude_devices_set.add(dev) + exclude_mapping[dev_name] = exclude_devices_set + return exclude_mapping + +DEFAULT_DEVICE_MAPPINGS = [] +DEFAULT_EXCLUDE_DEVICES = [] + +agent_opts = [ + cfg.IntOpt('polling_interval', default=2, + help=_("The number of seconds the agent will wait between " + "polling for local device changes.")), +] + +sriov_nic_opts = [ + cfg.ListOpt('physical_device_mappings', + default=DEFAULT_DEVICE_MAPPINGS, + help=_("List of : mapping " + "physical network names to the agent's node-specific " + "physical network device of SR-IOV physical " + "function to be used for VLAN networks. " + "All physical networks listed in network_vlan_ranges " + "on the server should have mappings to appropriate " + "interfaces on each agent")), + cfg.ListOpt('exclude_devices', + default=DEFAULT_EXCLUDE_DEVICES, + help=_("List of : " + "mapping network_device to the agent's node-specific " + "list of virtual functions that should not be used " + "for virtual networking. excluded_devices is a " + "semicolon separated list of virtual functions " + "(BDF format).to exclude from network_device. " + "The network_device in the mapping should appear in " + "the physical_device_mappings list.")), +] + + +cfg.CONF.register_opts(agent_opts, 'AGENT') +cfg.CONF.register_opts(sriov_nic_opts, 'SRIOV_NIC') +config.register_agent_state_opts_helper(cfg.CONF) +config.register_root_helper(cfg.CONF) diff --git a/neutron/plugins/sriovnicagent/common/exceptions.py b/neutron/plugins/sriovnicagent/common/exceptions.py new file mode 100644 index 0000000000..75a781723b --- /dev/null +++ b/neutron/plugins/sriovnicagent/common/exceptions.py @@ -0,0 +1,32 @@ +# 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 neutron.common import exceptions as n_exc + + +class SriovNicError(n_exc.NeutronException): + pass + + +class InvalidDeviceError(SriovNicError): + message = _("Invalid Device %(dev_name)s: %(reason)s") + + +class IpCommandError(SriovNicError): + message = _("ip command failed on device %(dev_name)s: %(reason)s") + + +class InvalidPciSlotError(SriovNicError): + message = _("Invalid pci slot %(pci_slot)s") diff --git a/neutron/plugins/sriovnicagent/eswitch_manager.py b/neutron/plugins/sriovnicagent/eswitch_manager.py new file mode 100644 index 0000000000..b535907705 --- /dev/null +++ b/neutron/plugins/sriovnicagent/eswitch_manager.py @@ -0,0 +1,283 @@ +# 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. + +# @author: Samer Deeb, Mellanox Technologies, Ltd + +import os +import re + +from neutron.openstack.common import log as logging +from neutron.plugins.sriovnicagent.common import exceptions as exc +from neutron.plugins.sriovnicagent import pci_lib + +LOG = logging.getLogger(__name__) + + +class PciOsWrapper(object): + """OS wrapper for checking virtual functions""" + + DEVICE_PATH = "/sys/class/net/%s/device" + PCI_PATH = "/sys/class/net/%s/device/virtfn%s/net" + VIRTFN_FORMAT = "^virtfn(?P\d+)" + VIRTFN_REG_EX = re.compile(VIRTFN_FORMAT) + + @classmethod + def scan_vf_devices(cls, dev_name): + """Scan os directories to get VF devices + + @param dev_name: pf network device name + @return: list of virtual functions + """ + vf_list = [] + dev_path = cls.DEVICE_PATH % dev_name + if not os.path.isdir(dev_path): + LOG.error(_("Failed to get devices for %s"), dev_name) + raise exc.InvalidDeviceError(dev_name=dev_name, + reason=_("Device not found")) + file_list = os.listdir(dev_path) + for file_name in file_list: + pattern_match = cls.VIRTFN_REG_EX.match(file_name) + if pattern_match: + vf_index = int(pattern_match.group("vf_index")) + file_path = os.path.join(dev_path, file_name) + if os.path.islink(file_path): + file_link = os.readlink(file_path) + pci_slot = os.path.basename(file_link) + vf_list.append((pci_slot, vf_index)) + if not vf_list: + raise exc.InvalidDeviceError( + dev_name=dev_name, + reason=_("Device has no virtual functions")) + return vf_list + + @classmethod + def is_assigned_vf(cls, dev_name, vf_index): + """Check if VF is assigned. + + Checks if a given vf index of a given device name is assigned + by checking the relevant path in the system + @param dev_name: pf network device name + @param vf_index: vf index + """ + path = cls.PCI_PATH % (dev_name, vf_index) + return not (os.path.isdir(path)) + + +class EmbSwitch(object): + """Class to manage logical embedded switch entity. + + Embedded Switch object is logical entity representing all VFs + connected to same physical network + Each physical network is mapped to PF network device interface, + meaning all its VF, excluding the devices in exclude_device list. + @ivar pci_slot_map: dictionary for mapping each pci slot to vf index + @ivar pci_dev_wrapper: pci device wrapper + """ + + def __init__(self, phys_net, dev_name, exclude_devices, root_helper): + """Constructor + + @param phys_net: physical network + @param dev_name: network device name + @param exclude_devices: list of pci slots to exclude + @param root_helper: root permissions helper + """ + self.phys_net = phys_net + self.dev_name = dev_name + self.pci_slot_map = {} + self.pci_dev_wrapper = pci_lib.PciDeviceIPWrapper(dev_name, + root_helper) + + self._load_devices(exclude_devices) + + def _load_devices(self, exclude_devices): + """Load devices from driver and filter if needed. + + @param exclude_devices: excluded devices mapping device_name: pci slots + """ + scanned_pci_list = PciOsWrapper.scan_vf_devices(self.dev_name) + for pci_slot, vf_index in scanned_pci_list: + if pci_slot not in exclude_devices: + self.pci_slot_map[pci_slot] = vf_index + + def get_pci_slot_list(self): + """Get list of VF addresses.""" + return self.pci_slot_map.keys() + + def get_assigned_devices(self): + """Get assigned Virtual Functions. + + @return: list of VF mac addresses + """ + vf_list = [] + assigned_macs = [] + for vf_index in self.pci_slot_map.itervalues(): + if not PciOsWrapper.is_assigned_vf(self.dev_name, vf_index): + continue + vf_list.append(vf_index) + if vf_list: + assigned_macs = self.pci_dev_wrapper.get_assigned_macs(vf_list) + return assigned_macs + + def get_device_state(self, pci_slot): + """Get device state. + + @param pci_slot: Virtual Function address + """ + vf_index = self.pci_slot_map.get(pci_slot) + if vf_index is None: + LOG.warning(_("Cannot find vf index for pci slot %s"), + pci_slot) + raise exc.InvalidPciSlotError(pci_slot=pci_slot) + return self.pci_dev_wrapper.get_vf_state(vf_index) + + def set_device_state(self, pci_slot, state): + """Set device state. + + @param pci_slot: Virtual Function address + @param state: link state + """ + vf_index = self.pci_slot_map.get(pci_slot) + if vf_index is None: + LOG.warning(_("Cannot find vf index for pci slot %s"), + pci_slot) + raise exc.InvalidPciSlotError(pci_slot=pci_slot) + return self.pci_dev_wrapper.set_vf_state(vf_index, state) + + def get_pci_device(self, pci_slot): + """Get mac address for given Virtual Function address + + @param pci_slot: pci slot + @return: MAC address of virtual function + """ + vf_index = self.pci_slot_map.get(pci_slot) + mac = None + if vf_index is not None: + if PciOsWrapper.is_assigned_vf(self.dev_name, vf_index): + macs = self.pci_dev_wrapper.get_assigned_macs([vf_index]) + if macs: + mac = macs[0] + return mac + + +class ESwitchManager(object): + """Manages logical Embedded Switch entities for physical network.""" + + def __init__(self, device_mappings, exclude_devices, root_helper): + """Constructor. + + Create Embedded Switch logical entities for all given device mappings, + using exclude devices. + """ + self.emb_switches_map = {} + self.pci_slot_map = {} + self.root_helper = root_helper + + self._discover_devices(device_mappings, exclude_devices) + + def device_exists(self, device_mac, pci_slot): + """Verify if device exists. + + Check if a device mac exists and matches the given VF pci slot + @param device_mac: device mac + @param pci_slot: VF address + """ + embedded_switch = self._get_emb_eswitch(device_mac, pci_slot) + if embedded_switch: + return True + return False + + def get_assigned_devices(self, phys_net=None): + """Get all assigned devices. + + Get all assigned devices belongs to given embedded switch + @param phys_net: physical network, if none get all assigned devices + @return: set of assigned VFs mac addresses + """ + if phys_net: + embedded_switch = self.emb_switches_map.get(phys_net, None) + if not embedded_switch: + return set() + eswitch_objects = [embedded_switch] + else: + eswitch_objects = self.emb_switches_map.values() + assigned_devices = set() + for embedded_switch in eswitch_objects: + for device_mac in embedded_switch.get_assigned_devices(): + assigned_devices.add(device_mac) + return assigned_devices + + def get_device_state(self, device_mac, pci_slot): + """Get device state. + + Get the device state (up/True or down/False) + @param device_mac: device mac + @param pci_slot: VF pci slot + @return: device state (True/False) None if failed + """ + embedded_switch = self._get_emb_eswitch(device_mac, pci_slot) + if embedded_switch: + return embedded_switch.get_device_state(pci_slot) + return False + + def set_device_state(self, device_mac, pci_slot, admin_state_up): + """Set device state + + Sets the device state (up or down) + @param device_mac: device mac + @param pci_slot: pci slot + @param admin_state_up: device admin state True/False + """ + embedded_switch = self._get_emb_eswitch(device_mac, pci_slot) + if embedded_switch: + embedded_switch.set_device_state(pci_slot, + admin_state_up) + + def _discover_devices(self, device_mappings, exclude_devices): + """Discover which Virtual functions to manage. + + Discover devices, and create embedded switch object for network device + @param device_mappings: device mapping physical_network:device_name + @param exclude_devices: excluded devices mapping device_name: pci slots + """ + if exclude_devices is None: + exclude_devices = {} + for phys_net, dev_name in device_mappings.iteritems(): + self._create_emb_switch(phys_net, dev_name, + exclude_devices.get(dev_name, set())) + + def _create_emb_switch(self, phys_net, dev_name, exclude_devices): + embedded_switch = EmbSwitch(phys_net, dev_name, exclude_devices, + self.root_helper) + self.emb_switches_map[phys_net] = embedded_switch + for pci_slot in embedded_switch.get_pci_slot_list(): + self.pci_slot_map[pci_slot] = embedded_switch + + def _get_emb_eswitch(self, device_mac, pci_slot): + """Get embedded switch. + + Get embedded switch by pci slot and validate pci has device mac + @param device_mac: device mac + @param pci_slot: pci slot + """ + embedded_switch = self.pci_slot_map.get(pci_slot) + if embedded_switch: + used_device_mac = embedded_switch.get_pci_device(pci_slot) + if used_device_mac != device_mac: + LOG.warning(_("device pci mismatch: %(device_mac)s " + "- %(pci_slot)s"), {"device_mac": device_mac, + "pci_slot": pci_slot}) + embedded_switch = None + return embedded_switch diff --git a/neutron/plugins/sriovnicagent/pci_lib.py b/neutron/plugins/sriovnicagent/pci_lib.py new file mode 100644 index 0000000000..0d615c3ca0 --- /dev/null +++ b/neutron/plugins/sriovnicagent/pci_lib.py @@ -0,0 +1,148 @@ +# 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. + +# @author: Samer Deeb, Mellanox Technologies, Ltd + +import re + +from neutron.agent.linux import ip_lib +from neutron.openstack.common import log as logging +from neutron.plugins.sriovnicagent.common import exceptions as exc + +LOG = logging.getLogger(__name__) + + +class PciDeviceIPWrapper(ip_lib.IPWrapper): + """Wrapper class for ip link commands. + + wrapper for getting/setting pci device details using ip link... + """ + VF_PATTERN = "^vf(\s+)(?P\d+)(\s+)" + MAC_PATTERN = "MAC(\s+)(?P[a-fA-F0-9:]+)," + STATE_PATTERN = "(\s+)link-state(\s+)(?P\w+)" + ANY_PATTERN = "(.*)," + + VF_LINE_FORMAT = VF_PATTERN + MAC_PATTERN + ANY_PATTERN + STATE_PATTERN + VF_DETAILS_REG_EX = re.compile(VF_LINE_FORMAT) + + class LinkState: + ENABLE = "enable" + DISABLE = "disable" + + def __init__(self, dev_name, root_helper=None): + super(ip_lib.IPWrapper, self).__init__(root_helper=root_helper) + self.dev_name = dev_name + + def get_assigned_macs(self, vf_list): + """Get assigned mac addresses for vf list. + + @param vf_list: list of vf indexes + @return: list of assigned mac addresses + """ + try: + out = self._execute('', "link", ("show", self.dev_name), + self.root_helper) + except Exception as e: + LOG.exception(_("Failed executing ip command")) + raise exc.IpCommandError(dev_name=self.dev_name, + reason=str(e)) + vf_lines = self._get_vf_link_show(vf_list, out) + vf_details_list = [] + if vf_lines: + for vf_line in vf_lines: + vf_details = self._parse_vf_link_show(vf_line) + if vf_details: + vf_details_list.append(vf_details) + return [vf_details.get("MAC") for vf_details in + vf_details_list] + + def get_vf_state(self, vf_index): + """Get vf state {True/False} + + @param vf_index: vf index + @todo: Handle "auto" state + """ + try: + out = self._execute('', "link", ("show", self.dev_name), + self.root_helper) + except Exception as e: + LOG.exception(_("Failed executing ip command")) + raise exc.IpCommandError(dev_name=self.dev_name, + reason=str(e)) + vf_lines = self._get_vf_link_show([vf_index], out) + if vf_lines: + vf_details = self._parse_vf_link_show(vf_lines[0]) + if vf_details: + state = vf_details.get("link-state", + self.LinkState.DISABLE) + if state != self.LinkState.DISABLE: + return True + return False + + def set_vf_state(self, vf_index, state): + """sets vf state. + + @param vf_index: vf index + @param state: required state {True/False} + """ + status_str = self.LinkState.ENABLE if state else \ + self.LinkState.DISABLE + + try: + self._execute('', "link", ("set", self.dev_name, "vf", + str(vf_index), "state", status_str), + self.root_helper) + except Exception as e: + LOG.exception(_("Failed executing ip command")) + raise exc.IpCommandError(dev_name=self.dev_name, + reason=str(e)) + + def _get_vf_link_show(self, vf_list, link_show_out): + """Get link show output for VFs + + get vf link show command output filtered by given vf list + @param vf_list: list of vf indexes + @param link_show_out: link show command output + @return: list of output rows regarding given vf_list + """ + vf_lines = [] + for line in link_show_out.split("\n"): + line = line.strip() + if line.startswith("vf"): + details = line.split() + index = int(details[1]) + if index in vf_list: + vf_lines.append(line) + if not vf_lines: + LOG.warning(_("Cannot find vfs %(vfs)s in device %(dev_name)s"), + {'vfs': vf_list, 'dev_name': self.dev_name}) + return vf_lines + + def _parse_vf_link_show(self, vf_line): + """Parses vf link show command output line. + + @param vf_line: link show vf line + """ + vf_details = {} + pattern_match = self.VF_DETAILS_REG_EX.match(vf_line) + if pattern_match: + vf_details["vf"] = int(pattern_match.group("vf_index")) + vf_details["MAC"] = pattern_match.group("mac") + vf_details["link-state"] = pattern_match.group("state") + else: + LOG.warning(_("failed to parse vf link show line %(line)s: " + "for %(device)s"), {'line': vf_line, + 'device': self.dev_name}) + return vf_details diff --git a/neutron/plugins/sriovnicagent/sriov_nic_agent.py b/neutron/plugins/sriovnicagent/sriov_nic_agent.py new file mode 100644 index 0000000000..886269307f --- /dev/null +++ b/neutron/plugins/sriovnicagent/sriov_nic_agent.py @@ -0,0 +1,355 @@ +# 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 socket +import sys +import time + +import eventlet +eventlet.monkey_patch() + +from oslo.config import cfg + +from neutron.agent import rpc as agent_rpc +from neutron.agent import securitygroups_rpc as sg_rpc +from neutron.common import config as common_config +from neutron.common import constants as q_constants +from neutron.common import rpc as n_rpc +from neutron.common import topics +from neutron.common import utils as q_utils +from neutron import context +from neutron.openstack.common import log as logging +from neutron.openstack.common import loopingcall +from neutron.plugins.sriovnicagent.common import config # noqa +from neutron.plugins.sriovnicagent.common import exceptions as exc +from neutron.plugins.sriovnicagent import eswitch_manager as esm + + +LOG = logging.getLogger(__name__) + + +class SriovNicSwitchRpcCallbacks(n_rpc.RpcCallback, + sg_rpc.SecurityGroupAgentRpcCallbackMixin): + + # Set RPC API version to 1.0 by default. + # history + # 1.1 Support Security Group RPC + RPC_API_VERSION = '1.1' + + def __init__(self, context, agent): + super(SriovNicSwitchRpcCallbacks, self).__init__() + self.context = context + self.agent = agent + self.sg_agent = agent + + def port_update(self, context, **kwargs): + LOG.debug("port_update received") + port = kwargs.get('port') + # Put the port mac address in the updated_devices set. + # Do not store port details, as if they're used for processing + # notifications there is no guarantee the notifications are + # processed in the same order as the relevant API requests. + self.agent.updated_devices.add(port['mac_address']) + LOG.debug(_("port_update RPC received for port: %s"), port['id']) + + +class SriovNicSwitchPluginApi(agent_rpc.PluginApi, + sg_rpc.SecurityGroupServerRpcApiMixin): + pass + + +class SriovNicSwitchAgent(sg_rpc.SecurityGroupAgentRpcMixin): + def __init__(self, physical_devices_mappings, exclude_devices, + polling_interval, root_helper): + + self.polling_interval = polling_interval + self.root_helper = root_helper + self.setup_eswitch_mgr(physical_devices_mappings, + exclude_devices) + configurations = {'device_mappings': physical_devices_mappings} + self.agent_state = { + 'binary': 'neutron-sriov-nic-agent', + 'host': cfg.CONF.host, + 'topic': q_constants.L2_AGENT_TOPIC, + 'configurations': configurations, + 'agent_type': q_constants.AGENT_TYPE_NIC_SWITCH, + 'start_flag': True} + + # Stores port update notifications for processing in the main loop + self.updated_devices = set() + self._setup_rpc() + self.init_firewall() + # Initialize iteration counter + self.iter_num = 0 + + def _setup_rpc(self): + self.agent_id = 'nic-switch-agent.%s' % socket.gethostname() + LOG.info(_("RPC agent_id: %s"), self.agent_id) + + self.topic = topics.AGENT + self.plugin_rpc = SriovNicSwitchPluginApi(topics.PLUGIN) + self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN) + # RPC network init + self.context = context.get_admin_context_without_session() + # Handle updates from service + self.endpoints = [SriovNicSwitchRpcCallbacks(self.context, self)] + # Define the listening consumers for the agent + consumers = [[topics.PORT, topics.UPDATE], + [topics.NETWORK, topics.DELETE], + [topics.SECURITY_GROUP, topics.UPDATE]] + self.connection = agent_rpc.create_consumers(self.endpoints, + self.topic, + consumers) + + report_interval = cfg.CONF.AGENT.report_interval + if report_interval: + heartbeat = loopingcall.FixedIntervalLoopingCall( + self._report_state) + heartbeat.start(interval=report_interval) + + def _report_state(self): + try: + devices = len(self.eswitch_mgr.get_assigned_devices()) + self.agent_state.get('configurations')['devices'] = devices + self.state_rpc.report_state(self.context, + self.agent_state) + self.agent_state.pop('start_flag', None) + except Exception: + LOG.exception(_("Failed reporting state!")) + + def setup_eswitch_mgr(self, device_mappings, exclude_devices={}): + self.eswitch_mgr = esm.ESwitchManager(device_mappings, + exclude_devices, + self.root_helper) + + def scan_devices(self, registered_devices, updated_devices): + curr_devices = self.eswitch_mgr.get_assigned_devices() + device_info = {} + device_info['current'] = curr_devices + device_info['added'] = curr_devices - registered_devices + # we don't want to process updates for devices that don't exist + device_info['updated'] = updated_devices & curr_devices + # we need to clean up after devices are removed + device_info['removed'] = registered_devices - curr_devices + return device_info + + def _device_info_has_changes(self, device_info): + return (device_info.get('added') + or device_info.get('updated') + or device_info.get('removed')) + + def process_network_devices(self, device_info): + resync_a = False + resync_b = False + + self.prepare_devices_filter(device_info.get('added')) + + if device_info.get('updated'): + self.refresh_firewall() + # Updated devices are processed the same as new ones, as their + # admin_state_up may have changed. The set union prevents duplicating + # work when a device is new and updated in the same polling iteration. + devices_added_updated = (set(device_info.get('added')) + | set(device_info.get('updated'))) + if devices_added_updated: + resync_a = self.treat_devices_added_updated(devices_added_updated) + + if device_info.get('removed'): + resync_b = self.treat_devices_removed(device_info['removed']) + # If one of the above operations fails => resync with plugin + return (resync_a | resync_b) + + def treat_device(self, device, pci_slot, admin_state_up): + if self.eswitch_mgr.device_exists(device, pci_slot): + try: + self.eswitch_mgr.set_device_state(device, pci_slot, + admin_state_up) + except exc.SriovNicError: + LOG.exception(_("Failed to set device %s state"), device) + return + if admin_state_up: + # update plugin about port status + self.plugin_rpc.update_device_up(self.context, + device, + self.agent_id, + cfg.CONF.host) + else: + self.plugin_rpc.update_device_down(self.context, + device, + self.agent_id, + cfg.CONF.host) + else: + LOG.info(_("No device with MAC %s defined on agent."), device) + + def treat_devices_added_updated(self, devices): + try: + devices_details_list = self.plugin_rpc.get_devices_details_list( + self.context, devices, self.agent_id) + except Exception as e: + LOG.debug("Unable to get port details for devices " + "with MAC address %(devices)s: %(e)s", + {'devices': devices, 'e': e}) + # resync is needed + return True + + for device_details in devices_details_list: + device = device_details['device'] + LOG.debug("Port with MAC address %s is added", device) + + if 'port_id' in device_details: + LOG.info(_("Port %(device)s updated. Details: %(details)s"), + {'device': device, 'details': device_details}) + profile = device_details['profile'] + self.treat_device(device_details['device'], + profile.get('pci_slot'), + device_details['admin_state_up']) + else: + LOG.info(_("Device with MAC %s not defined on plugin"), device) + return False + + def treat_devices_removed(self, devices): + resync = False + for device in devices: + LOG.info(_("Removing device with mac_address %s"), device) + try: + dev_details = self.plugin_rpc.update_device_down(self.context, + device, + self.agent_id, + cfg.CONF.host) + except Exception as e: + LOG.debug(_("Removing port failed for device %(device)s " + "due to %(exc)s"), {'device': device, 'exc': e}) + resync = True + continue + if dev_details['exists']: + LOG.info(_("Port %s updated."), device) + else: + LOG.debug(_("Device %s not defined on plugin"), device) + return resync + + def daemon_loop(self): + sync = True + devices = set() + + LOG.info(_("SRIOV NIC Agent RPC Daemon Started!")) + + while True: + start = time.time() + LOG.debug("Agent rpc_loop - iteration:%d started", + self.iter_num) + if sync: + LOG.info(_("Agent out of sync with plugin!")) + devices.clear() + sync = False + device_info = {} + # Save updated devices dict to perform rollback in case + # resync would be needed, and then clear self.updated_devices. + # As the greenthread should not yield between these + # two statements, this will should be thread-safe. + updated_devices_copy = self.updated_devices + self.updated_devices = set() + try: + device_info = self.scan_devices(devices, updated_devices_copy) + if self._device_info_has_changes(device_info): + LOG.debug(_("Agent loop found changes! %s"), device_info) + # If treat devices fails - indicates must resync with + # plugin + sync = self.process_network_devices(device_info) + devices = device_info['current'] + except Exception: + LOG.exception(_("Error in agent loop. Devices info: %s"), + device_info) + sync = True + # Restore devices that were removed from this set earlier + # without overwriting ones that may have arrived since. + self.updated_devices |= updated_devices_copy + + # sleep till end of polling interval + elapsed = (time.time() - start) + if (elapsed < self.polling_interval): + time.sleep(self.polling_interval - elapsed) + else: + LOG.debug(_("Loop iteration exceeded interval " + "(%(polling_interval)s vs. %(elapsed)s)!"), + {'polling_interval': self.polling_interval, + 'elapsed': elapsed}) + self.iter_num = self.iter_num + 1 + + +class SriovNicAgentConfigParser(object): + def __init__(self): + self.device_mappings = {} + self.exclude_devices = {} + + def parse(self): + """Parses device_mappings and exclude_devices. + + Parse and validate the consistency in both mappings + """ + self.device_mappings = q_utils.parse_mappings( + cfg.CONF.SRIOV_NIC.physical_device_mappings) + self.exclude_devices = config.parse_exclude_devices( + cfg.CONF.SRIOV_NIC.exclude_devices) + self._validate() + + def _validate(self): + """ Validate configuration. + + Validate that network_device in excluded_device + exists in device mappings + """ + dev_net_set = set(self.device_mappings.itervalues()) + for dev_name in self.exclude_devices.iterkeys(): + if dev_name not in dev_net_set: + raise ValueError(_("Device name %(dev_name)s is missing from " + "physical_device_mappings") % {'dev_name': + dev_name}) + + +def main(): + common_config.init(sys.argv[1:]) + + common_config.setup_logging(cfg.CONF) + try: + config_parser = SriovNicAgentConfigParser() + config_parser.parse() + device_mappings = config_parser.device_mappings + exclude_devices = config_parser.exclude_devices + + except ValueError as e: + LOG.error(_("Failed on Agent configuration parse : %s." + " Agent terminated!"), e) + raise SystemExit(1) + LOG.info(_("Physical Devices mappings: %s"), device_mappings) + LOG.info(_("Exclude Devices: %s"), exclude_devices) + + polling_interval = cfg.CONF.AGENT.polling_interval + root_helper = cfg.CONF.AGENT.root_helper + try: + agent = SriovNicSwitchAgent(device_mappings, + exclude_devices, + polling_interval, + root_helper) + except exc.SriovNicError: + LOG.exception(_("Agent Initialization Failed")) + raise SystemExit(1) + # Start everything. + LOG.info(_("Agent initialized successfully, now running... ")) + agent.daemon_loop() + + +if __name__ == '__main__': + main() diff --git a/neutron/tests/unit/sriovnicagent/__init__.py b/neutron/tests/unit/sriovnicagent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/neutron/tests/unit/sriovnicagent/test_eswitch_manager.py b/neutron/tests/unit/sriovnicagent/test_eswitch_manager.py new file mode 100644 index 0000000000..a7c41e3af1 --- /dev/null +++ b/neutron/tests/unit/sriovnicagent/test_eswitch_manager.py @@ -0,0 +1,364 @@ +# 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 contextlib +import os + +import mock +import testtools + + +from neutron.plugins.sriovnicagent.common import exceptions as exc +from neutron.plugins.sriovnicagent import eswitch_manager as esm +from neutron.tests import base + + +class TestCreateESwitchManager(base.BaseTestCase): + SCANNED_DEVICES = [('0000:06:00.1', 0), + ('0000:06:00.2', 1), + ('0000:06:00.3', 2)] + + def test_create_eswitch_mgr_fail(self): + device_mappings = {'physnet1': 'p6p1'} + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.scan_vf_devices", + side_effect=exc.InvalidDeviceError(dev_name="p6p1", + reason="device" + " not found")), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + + with testtools.ExpectedException(exc.InvalidDeviceError): + esm.ESwitchManager(device_mappings, None, None) + + def test_create_eswitch_mgr_ok(self): + device_mappings = {'physnet1': 'p6p1'} + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.scan_vf_devices", + return_value=self.SCANNED_DEVICES), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + + esm.ESwitchManager(device_mappings, None, None) + + +class TestESwitchManagerApi(base.BaseTestCase): + SCANNED_DEVICES = [('0000:06:00.1', 0), + ('0000:06:00.2', 1), + ('0000:06:00.3', 2)] + + ASSIGNED_MAC = '00:00:00:00:00:66' + PCI_SLOT = '0000:06:00.1' + WRONG_MAC = '00:00:00:00:00:67' + WRONG_PCI = "0000:06:00.6" + + def setUp(self): + super(TestESwitchManagerApi, self).setUp() + device_mappings = {'physnet1': 'p6p1'} + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.scan_vf_devices", + return_value=self.SCANNED_DEVICES), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + self.eswitch_mgr = esm.ESwitchManager(device_mappings, None, None) + + def test_get_assigned_devices(self): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_assigned_devices", + return_value=[self.ASSIGNED_MAC]): + result = self.eswitch_mgr.get_assigned_devices() + self.assertEqual(set([self.ASSIGNED_MAC]), result) + + def test_get_device_status_true(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_device_state", + return_value=True)): + result = self.eswitch_mgr.get_device_state(self.ASSIGNED_MAC, + self.PCI_SLOT) + self.assertTrue(result) + + def test_get_device_status_false(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_device_state", + return_value=False)): + result = self.eswitch_mgr.get_device_state(self.ASSIGNED_MAC, + self.PCI_SLOT) + self.assertFalse(result) + + def test_get_device_status_mismatch(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_device_state", + return_value=True)): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "LOG.warning") as log_mock: + result = self.eswitch_mgr.get_device_state(self.WRONG_MAC, + self.PCI_SLOT) + log_mock.assert_called_with('device pci mismatch: ' + '%(device_mac)s - %(pci_slot)s', + {'pci_slot': self.PCI_SLOT, + 'device_mac': self.WRONG_MAC}) + self.assertFalse(result) + + def test_set_device_status(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.set_device_state")): + self.eswitch_mgr.set_device_state(self.ASSIGNED_MAC, + self.PCI_SLOT, True) + + def test_set_device_status_mismatch(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.set_device_state")): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "LOG.warning") as log_mock: + self.eswitch_mgr.set_device_state(self.WRONG_MAC, + self.PCI_SLOT, True) + log_mock.assert_called_with('device pci mismatch: ' + '%(device_mac)s - %(pci_slot)s', + {'pci_slot': self.PCI_SLOT, + 'device_mac': self.WRONG_MAC}) + + def _mock_device_exists(self, pci_slot, mac_address, expected_result): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC): + result = self.eswitch_mgr.device_exists(mac_address, + pci_slot) + self.assertEqual(expected_result, result) + + def test_device_exists_true(self): + self._mock_device_exists(self.PCI_SLOT, + self.ASSIGNED_MAC, + True) + + def test_device_exists_false(self): + self._mock_device_exists(self.WRONG_PCI, + self.WRONG_MAC, + False) + + def test_device_exists_mismatch(self): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "EmbSwitch.get_pci_device", + return_value=self.ASSIGNED_MAC): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "LOG.warning") as log_mock: + result = self.eswitch_mgr.device_exists(self.WRONG_MAC, + self.PCI_SLOT) + log_mock.assert_called_with('device pci mismatch: ' + '%(device_mac)s - %(pci_slot)s', + {'pci_slot': self.PCI_SLOT, + 'device_mac': self.WRONG_MAC}) + self.assertFalse(result) + + +class TestEmbSwitch(base.BaseTestCase): + DEV_NAME = "eth2" + PHYS_NET = "default" + ASSIGNED_MAC = '00:00:00:00:00:66' + PCI_SLOT = "0000:06:00.1" + WRONG_PCI_SLOT = "0000:06:00.4" + SCANNED_DEVICES = [('0000:06:00.1', 0), + ('0000:06:00.2', 1), + ('0000:06:00.3', 2)] + + def setUp(self): + super(TestEmbSwitch, self).setUp() + exclude_devices = set() + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.scan_vf_devices", + return_value=self.SCANNED_DEVICES): + self.emb_switch = esm.EmbSwitch(self.PHYS_NET, self.DEV_NAME, + exclude_devices, None) + + def test_get_assigned_devices(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.get_assigned_macs", + return_value=[self.ASSIGNED_MAC]), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + result = self.emb_switch.get_assigned_devices() + self.assertEqual([self.ASSIGNED_MAC], result) + + def test_get_assigned_devices_empty(self): + with mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=False): + result = self.emb_switch.get_assigned_devices() + self.assertFalse(result) + + def test_get_device_state_ok(self): + with mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.get_vf_state", + return_value=False): + result = self.emb_switch.get_device_state(self.PCI_SLOT) + self.assertFalse(result) + + def test_get_device_state_fail(self): + with mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.get_vf_state", + return_value=False): + self.assertRaises(exc.InvalidPciSlotError, + self.emb_switch.get_device_state, + self.WRONG_PCI_SLOT) + + def test_set_device_state_ok(self): + with mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.set_vf_state"): + with mock.patch("neutron.plugins.sriovnicagent.pci_lib.LOG." + "warning") as log_mock: + self.emb_switch.set_device_state(self.PCI_SLOT, True) + self.assertEqual(0, log_mock.call_count) + + def test_set_device_state_fail(self): + with mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.set_vf_state"): + self.assertRaises(exc.InvalidPciSlotError, + self.emb_switch.set_device_state, + self.WRONG_PCI_SLOT, True) + + def test_get_pci_device(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.get_assigned_macs", + return_value=[self.ASSIGNED_MAC]), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + result = self.emb_switch.get_pci_device(self.PCI_SLOT) + self.assertEqual(self.ASSIGNED_MAC, result) + + def test_get_pci_device_fail(self): + with contextlib.nested( + mock.patch("neutron.plugins.sriovnicagent.pci_lib." + "PciDeviceIPWrapper.get_assigned_macs", + return_value=[self.ASSIGNED_MAC]), + mock.patch("neutron.plugins.sriovnicagent.eswitch_manager." + "PciOsWrapper.is_assigned_vf", + return_value=True)): + result = self.emb_switch.get_pci_device(self.WRONG_PCI_SLOT) + self.assertIsNone(result) + + def test_get_pci_list(self): + result = self.emb_switch.get_pci_slot_list() + self.assertEqual([tup[0] for tup in self.SCANNED_DEVICES], result) + + +class TestPciOsWrapper(base.BaseTestCase): + DEV_NAME = "p7p1" + VF_INDEX = 1 + DIR_CONTENTS = [ + "mlx4_port1", + "virtfn0", + "virtfn1", + "virtfn2" + ] + DIR_CONTENTS_NO_MATCH = [ + "mlx4_port1", + "mlx4_port1" + ] + LINKS = { + "virtfn0": "../0000:04:00.1", + "virtfn1": "../0000:04:00.2", + "virtfn2": "../0000:04:00.3" + } + PCI_SLOTS = [ + ('0000:04:00.1', 0), + ('0000:04:00.2', 1), + ('0000:04:00.3', 2) + ] + + def test_scan_vf_devices(self): + def _get_link(file_path): + file_name = os.path.basename(file_path) + return self.LINKS[file_name] + + with contextlib.nested( + mock.patch("os.path.isdir", + return_value=True), + mock.patch("os.listdir", + return_value=self.DIR_CONTENTS), + mock.patch("os.path.islink", + return_value=True), + mock.patch("os.readlink", + side_effect=_get_link),): + result = esm.PciOsWrapper.scan_vf_devices(self.DEV_NAME) + self.assertEqual(self.PCI_SLOTS, result) + + def test_scan_vf_devices_no_dir(self): + with mock.patch("os.path.isdir", return_value=False): + self.assertRaises(exc.InvalidDeviceError, + esm.PciOsWrapper.scan_vf_devices, + self.DEV_NAME) + + def test_scan_vf_devices_no_content(self): + with contextlib.nested( + mock.patch("os.path.isdir", + return_value=True), + mock.patch("os.listdir", + return_value=[])): + self.assertRaises(exc.InvalidDeviceError, + esm.PciOsWrapper.scan_vf_devices, + self.DEV_NAME) + + def test_scan_vf_devices_no_match(self): + with contextlib.nested( + mock.patch("os.path.isdir", + return_value=True), + mock.patch("os.listdir", + return_value=self.DIR_CONTENTS_NO_MATCH)): + self.assertRaises(exc.InvalidDeviceError, + esm.PciOsWrapper.scan_vf_devices, + self.DEV_NAME) + + def _mock_assign_vf(self, dir_exists): + with mock.patch("os.path.isdir", + return_value=dir_exists): + result = esm.PciOsWrapper.is_assigned_vf(self.DEV_NAME, + self.VF_INDEX) + self.assertEqual(not dir_exists, result) + + def test_is_assigned_vf_true(self): + self._mock_assign_vf(True) + + def test_is_assigned_vf_false(self): + self._mock_assign_vf(False) diff --git a/neutron/tests/unit/sriovnicagent/test_pci_lib.py b/neutron/tests/unit/sriovnicagent/test_pci_lib.py new file mode 100644 index 0000000000..17862c9185 --- /dev/null +++ b/neutron/tests/unit/sriovnicagent/test_pci_lib.py @@ -0,0 +1,100 @@ +# 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 neutron.plugins.sriovnicagent.common import exceptions as exc +from neutron.plugins.sriovnicagent import pci_lib +from neutron.tests import base + + +class TestPciLib(base.BaseTestCase): + DEV_NAME = "p7p1" + VF_INDEX = 1 + VF_INDEX_DISABLE = 0 + PF_LINK_SHOW = ('122: p7p1: mtu 1500 qdisc noop' + ' state DOWN mode DEFAULT group default qlen 1000') + PF_MAC = ' link/ether f4:52:14:2a:3e:c0 brd ff:ff:ff:ff:ff:ff' + VF_0_LINK_SHOW = (' vf 0 MAC fa:16:3e:b4:81:ac, vlan 4095, spoof' + ' checking off, link-state disable') + VF_1_LINK_SHOW = (' vf 1 MAC 00:00:00:00:00:11, vlan 4095, spoof' + ' checking off, link-state enable') + VF_2_LINK_SHOW = (' vf 2 MAC fa:16:3e:68:4e:79, vlan 4095, spoof' + ' checking off, link-state enable') + VF_LINK_SHOW = '\n'.join((PF_LINK_SHOW, PF_MAC, VF_0_LINK_SHOW, + VF_1_LINK_SHOW, VF_2_LINK_SHOW)) + + MAC_MAPPING = { + 0: "fa:16:3e:b4:81:ac", + 1: "00:00:00:00:00:11", + 2: "fa:16:3e:68:4e:79", + } + + def setUp(self): + super(TestPciLib, self).setUp() + self.pci_wrapper = pci_lib.PciDeviceIPWrapper(self.DEV_NAME) + + def test_get_assigned_macs(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.return_value = self.VF_LINK_SHOW + result = self.pci_wrapper.get_assigned_macs([self.VF_INDEX]) + self.assertEqual([self.MAC_MAPPING[self.VF_INDEX]], result) + + def test_get_assigned_macs_fail(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.side_effect = Exception() + self.assertRaises(exc.IpCommandError, + self.pci_wrapper.get_assigned_macs, + [self.VF_INDEX]) + + def test_get_vf_state_enable(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.return_value = self.VF_LINK_SHOW + result = self.pci_wrapper.get_vf_state(self.VF_INDEX) + self.assertTrue(result) + + def test_get_vf_state_disable(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.return_value = self.VF_LINK_SHOW + result = self.pci_wrapper.get_vf_state(self.VF_INDEX_DISABLE) + self.assertFalse(result) + + def test_get_vf_state_fail(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.side_effect = Exception() + self.assertRaises(exc.IpCommandError, + self.pci_wrapper.get_vf_state, + self.VF_INDEX) + + def test_set_vf_state(self): + with mock.patch.object(self.pci_wrapper, "_execute"): + result = self.pci_wrapper.set_vf_state(self.VF_INDEX, + True) + self.assertIsNone(result) + + def test_set_vf_state_fail(self): + with mock.patch.object(self.pci_wrapper, + "_execute") as mock_exec: + mock_exec.side_effect = Exception() + self.assertRaises(exc.IpCommandError, + self.pci_wrapper.set_vf_state, + self.VF_INDEX, + True) diff --git a/neutron/tests/unit/sriovnicagent/test_sriov_agent_config.py b/neutron/tests/unit/sriovnicagent/test_sriov_agent_config.py new file mode 100644 index 0000000000..d77a9f3224 --- /dev/null +++ b/neutron/tests/unit/sriovnicagent/test_sriov_agent_config.py @@ -0,0 +1,127 @@ +# 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 utils as q_utils +from neutron.plugins.sriovnicagent.common import config +from neutron.plugins.sriovnicagent import sriov_nic_agent as agent +from neutron.tests import base + + +class TestSriovAgentConfig(base.BaseTestCase): + EXCLUDE_DEVICES_LIST = ['p7p1:0000:07:00.1;0000:07:00.2', + 'p3p1:0000:04:00.3'] + + EXCLUDE_DEVICES_LIST_INVALID = ['p7p2:0000:07:00.1;0000:07:00.2'] + + EXCLUDE_DEVICES_WITH_SPACES_LIST = ['p7p1: 0000:07:00.1 ; 0000:07:00.2', + 'p3p1:0000:04:00.3 '] + + EXCLUDE_DEVICES_WITH_SPACES_ERROR = ['p7p1', + 'p3p1:0000:04:00.3 '] + + EXCLUDE_DEVICES = {'p7p1': set(['0000:07:00.1', '0000:07:00.2']), + 'p3p1': set(['0000:04:00.3'])} + + DEVICE_MAPPING_LIST = ['physnet7:p7p1', + 'physnet3:p3p1'] + + DEVICE_MAPPING_WITH_ERROR_LIST = ['physnet7', + 'physnet3:p3p1'] + + DEVICE_MAPPING_WITH_SPACES_LIST = ['physnet7 : p7p1', + 'physnet3 : p3p1 '] + DEVICE_MAPPING = {'physnet7': 'p7p1', + 'physnet3': 'p3p1'} + + def test_defaults(self): + self.assertEqual(config.DEFAULT_DEVICE_MAPPINGS, + cfg.CONF.SRIOV_NIC.physical_device_mappings) + self.assertEqual(config.DEFAULT_EXCLUDE_DEVICES, + cfg.CONF.SRIOV_NIC.exclude_devices) + self.assertEqual(2, + cfg.CONF.AGENT.polling_interval) + + def test_device_mappings(self): + cfg.CONF.set_override('physical_device_mappings', + self.DEVICE_MAPPING_LIST, + 'SRIOV_NIC') + device_mappings = q_utils.parse_mappings( + cfg.CONF.SRIOV_NIC.physical_device_mappings) + self.assertEqual(device_mappings, self.DEVICE_MAPPING) + + def test_device_mappings_with_error(self): + cfg.CONF.set_override('physical_device_mappings', + self.DEVICE_MAPPING_WITH_ERROR_LIST, + 'SRIOV_NIC') + self.assertRaises(ValueError, q_utils.parse_mappings, + cfg.CONF.SRIOV_NIC.physical_device_mappings) + + def test_device_mappings_with_spaces(self): + cfg.CONF.set_override('physical_device_mappings', + self.DEVICE_MAPPING_WITH_SPACES_LIST, + 'SRIOV_NIC') + device_mappings = q_utils.parse_mappings( + cfg.CONF.SRIOV_NIC.physical_device_mappings) + self.assertEqual(device_mappings, self.DEVICE_MAPPING) + + def test_exclude_devices(self): + cfg.CONF.set_override('exclude_devices', + self.EXCLUDE_DEVICES_LIST, + 'SRIOV_NIC') + exclude_devices = config.parse_exclude_devices( + cfg.CONF.SRIOV_NIC.exclude_devices) + self.assertEqual(exclude_devices, self.EXCLUDE_DEVICES) + + def test_exclude_devices_with_spaces(self): + cfg.CONF.set_override('exclude_devices', + self.EXCLUDE_DEVICES_WITH_SPACES_LIST, + 'SRIOV_NIC') + exclude_devices = config.parse_exclude_devices( + cfg.CONF.SRIOV_NIC.exclude_devices) + self.assertEqual(exclude_devices, self.EXCLUDE_DEVICES) + + def test_exclude_devices_with_error(self): + cfg.CONF.set_override('exclude_devices', + self.EXCLUDE_DEVICES_WITH_SPACES_ERROR, + 'SRIOV_NIC') + self.assertRaises(ValueError, config.parse_exclude_devices, + cfg.CONF.SRIOV_NIC.exclude_devices) + + def test_validate_config_ok(self): + cfg.CONF.set_override('physical_device_mappings', + self.DEVICE_MAPPING_LIST, + 'SRIOV_NIC') + cfg.CONF.set_override('exclude_devices', + self.EXCLUDE_DEVICES_LIST, + 'SRIOV_NIC') + config_parser = agent.SriovNicAgentConfigParser() + config_parser.parse() + device_mappings = config_parser.device_mappings + exclude_devices = config_parser.exclude_devices + self.assertEqual(exclude_devices, self.EXCLUDE_DEVICES) + self.assertEqual(device_mappings, self.DEVICE_MAPPING) + + def test_validate_config_fail(self): + cfg.CONF.set_override('physical_device_mappings', + self.DEVICE_MAPPING_LIST, + 'SRIOV_NIC') + cfg.CONF.set_override('exclude_devices', + self.EXCLUDE_DEVICES_LIST_INVALID, + 'SRIOV_NIC') + config_parser = agent.SriovNicAgentConfigParser() + self.assertRaises(ValueError, config_parser.parse) diff --git a/neutron/tests/unit/sriovnicagent/test_sriov_neutron_agent.py b/neutron/tests/unit/sriovnicagent/test_sriov_neutron_agent.py new file mode 100644 index 0000000000..ac078e267b --- /dev/null +++ b/neutron/tests/unit/sriovnicagent/test_sriov_neutron_agent.py @@ -0,0 +1,217 @@ +# 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.plugins.sriovnicagent.common import config # noqa +from neutron.plugins.sriovnicagent import sriov_nic_agent +from neutron.tests import base + +DEVICE_MAC = '11:22:33:44:55:66' + + +class TestSriovAgent(base.BaseTestCase): + def setUp(self): + super(TestSriovAgent, self).setUp() + # disable setting up periodic state reporting + cfg.CONF.set_override('report_interval', 0, 'AGENT') + cfg.CONF.set_override('rpc_backend', + 'neutron.openstack.common.rpc.impl_fake') + cfg.CONF.set_default('firewall_driver', + 'neutron.agent.firewall.NoopFirewallDriver', + group='SECURITYGROUP') + cfg.CONF.set_default('enable_security_group', + False, + group='SECURITYGROUP') + + class MockFixedIntervalLoopingCall(object): + def __init__(self, f): + self.f = f + + def start(self, interval=0): + self.f() + + mock.patch('neutron.openstack.common.loopingcall.' + 'FixedIntervalLoopingCall', + new=MockFixedIntervalLoopingCall) + + self.agent = sriov_nic_agent.SriovNicSwitchAgent({}, {}, 0, None) + + def test_treat_devices_removed_with_existed_device(self): + agent = sriov_nic_agent.SriovNicSwitchAgent({}, {}, 0, None) + devices = [DEVICE_MAC] + with mock.patch.object(agent.plugin_rpc, + "update_device_down") as fn_udd: + fn_udd.return_value = {'device': DEVICE_MAC, + 'exists': True} + with mock.patch.object(sriov_nic_agent.LOG, + 'info') as log: + resync = agent.treat_devices_removed(devices) + self.assertEqual(2, log.call_count) + self.assertFalse(resync) + self.assertTrue(fn_udd.called) + + def test_treat_devices_removed_with_not_existed_device(self): + agent = sriov_nic_agent.SriovNicSwitchAgent({}, {}, 0, None) + devices = [DEVICE_MAC] + with mock.patch.object(agent.plugin_rpc, + "update_device_down") as fn_udd: + fn_udd.return_value = {'device': DEVICE_MAC, + 'exists': False} + with mock.patch.object(sriov_nic_agent.LOG, + 'debug') as log: + resync = agent.treat_devices_removed(devices) + self.assertEqual(1, log.call_count) + self.assertFalse(resync) + self.assertTrue(fn_udd.called) + + def test_treat_devices_removed_failed(self): + agent = sriov_nic_agent.SriovNicSwitchAgent({}, {}, 0, None) + devices = [DEVICE_MAC] + with mock.patch.object(agent.plugin_rpc, + "update_device_down") as fn_udd: + fn_udd.side_effect = Exception() + with mock.patch.object(sriov_nic_agent.LOG, + 'debug') as log: + resync = agent.treat_devices_removed(devices) + self.assertEqual(1, log.call_count) + self.assertTrue(resync) + self.assertTrue(fn_udd.called) + + def mock_scan_devices(self, expected, mock_current, + registered_devices, updated_devices): + self.agent.eswitch_mgr = mock.Mock() + self.agent.eswitch_mgr.get_assigned_devices.return_value = mock_current + + results = self.agent.scan_devices(registered_devices, updated_devices) + self.assertEqual(expected, results) + + def test_scan_devices_returns_empty_sets(self): + registered = set() + updated = set() + mock_current = set() + expected = {'current': set(), + 'updated': set(), + 'added': set(), + 'removed': set()} + self.mock_scan_devices(expected, mock_current, registered, updated) + + def test_scan_devices_no_changes(self): + registered = set(['1', '2']) + updated = set() + mock_current = set(['1', '2']) + expected = {'current': set(['1', '2']), + 'updated': set(), + 'added': set(), + 'removed': set()} + self.mock_scan_devices(expected, mock_current, registered, updated) + + def test_scan_devices_new_and_removed(self): + registered = set(['1', '2']) + updated = set() + mock_current = set(['2', '3']) + expected = {'current': set(['2', '3']), + 'updated': set(), + 'added': set(['3']), + 'removed': set(['1'])} + self.mock_scan_devices(expected, mock_current, registered, updated) + + def test_scan_devices_new_updates(self): + registered = set(['1']) + updated = set(['2']) + mock_current = set(['1', '2']) + expected = {'current': set(['1', '2']), + 'updated': set(['2']), + 'added': set(['2']), + 'removed': set()} + self.mock_scan_devices(expected, mock_current, registered, updated) + + def test_scan_devices_updated_missing(self): + registered = set(['1']) + updated = set(['2']) + mock_current = set(['1']) + expected = {'current': set(['1']), + 'updated': set(), + 'added': set(), + 'removed': set()} + self.mock_scan_devices(expected, mock_current, registered, updated) + + def test_process_network_devices(self): + agent = self.agent + device_info = {'current': set(), + 'added': set(['mac3', 'mac4']), + 'updated': set(['mac2', 'mac3']), + 'removed': set(['mac1'])} + agent.prepare_devices_filter = mock.Mock() + agent.refresh_firewall = mock.Mock() + agent.treat_devices_added_updated = mock.Mock(return_value=False) + agent.treat_devices_removed = mock.Mock(return_value=False) + + agent.process_network_devices(device_info) + + agent.prepare_devices_filter.assert_called_with(set(['mac3', 'mac4'])) + self.assertTrue(agent.refresh_firewall.called) + agent.treat_devices_added_updated.assert_called_with(set(['mac2', + 'mac3', + 'mac4'])) + agent.treat_devices_removed.assert_called_with(set(['mac1'])) + + def test_treat_devices_added_updated_admin_state_up_true(self): + agent = self.agent + mock_details = {'device': 'aa:bb:cc:dd:ee:ff', + 'port_id': 'port123', + 'network_id': 'net123', + 'admin_state_up': True, + 'network_type': 'vlan', + 'segmentation_id': 100, + 'profile': {'pci_slot': '1:2:3.0'}, + 'physical_network': 'physnet1'} + agent.plugin_rpc = mock.Mock() + agent.plugin_rpc.get_devices_details_list.return_value = [mock_details] + agent.eswitch_mgr = mock.Mock() + agent.eswitch_mgr.device_exists.return_value = True + agent.set_device_state = mock.Mock() + resync_needed = agent.treat_devices_added_updated( + set(['aa:bb:cc:dd:ee:ff'])) + + self.assertFalse(resync_needed) + agent.eswitch_mgr.device_exists.assert_called_with('aa:bb:cc:dd:ee:ff', + '1:2:3.0') + agent.eswitch_mgr.set_device_state.assert_called_with( + 'aa:bb:cc:dd:ee:ff', + '1:2:3.0', + True) + self.assertTrue(agent.plugin_rpc.update_device_up.called) + + def test_treat_devices_added_updated_admin_state_up_false(self): + agent = self.agent + mock_details = {'device': 'aa:bb:cc:dd:ee:ff', + 'port_id': 'port123', + 'network_id': 'net123', + 'admin_state_up': False, + 'network_type': 'vlan', + 'segmentation_id': 100, + 'profile': {'pci_slot': '1:2:3.0'}, + 'physical_network': 'physnet1'} + agent.plugin_rpc = mock.Mock() + agent.plugin_rpc.get_devices_details_list.return_value = [mock_details] + agent.remove_port_binding = mock.Mock() + resync_needed = agent.treat_devices_added_updated( + set(['aa:bb:cc:dd:ee:ff'])) + + self.assertFalse(resync_needed) + self.assertFalse(agent.plugin_rpc.update_device_up.called) diff --git a/setup.cfg b/setup.cfg index 2bbcd1a460..5770756ac3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -117,6 +117,7 @@ console_scripts = neutron-vpn-agent = neutron.services.vpn.agent:main neutron-metering-agent = neutron.services.metering.agents.metering_agent:main neutron-ofagent-agent = neutron.plugins.ofagent.agent.main:main + neutron-sriov-nic-agent = neutron.plugins.sriovnicagent.sriov_nic_agent:main neutron-sanity-check = neutron.cmd.sanity_check:main neutron.core_plugins = bigswitch = neutron.plugins.bigswitch.plugin:NeutronRestProxyV2