From b4b1db30db41e418bc1774bb45c7d32be87e16be Mon Sep 17 00:00:00 2001 From: Irena Berezovsky Date: Wed, 16 Jul 2014 14:33:42 +0300 Subject: [PATCH] ML2 mechanism driver for SR-IOV capable NIC based switching, Part 2 This set of changes introduces SRIOV NIC Agent to run with ML2 mechanism driver for SR-IOV capable NIC based switching. This is the second part of a 2 part commit. The review is submitted in two parts: - Part 1 The Mechanism Driver to support port binding for SR-IOV virtual functions of SRIOV capable switching NICs. - Part2 (this part) The SRIOV NIC Based L2 Agent. Use configurable list of mappings physical_networks to PF interfaces and configurable list of mappings PF interfaces to list of excluded VFs to get list of Virtual Functions that agent should manage. Current implementation supports admin state updates. Co-authored-by: Samer Deeb Partially implements: blueprint ml2-sriov-nic-switch Change-Id: I533ccee067935326d5837f90ba321a962e8dc2a6 --- etc/neutron/plugins/ml2/ml2_conf_sriov.ini | 25 +- neutron/plugins/sriovnicagent/__init__.py | 0 .../plugins/sriovnicagent/common/__init__.py | 0 .../plugins/sriovnicagent/common/config.py | 88 +++++ .../sriovnicagent/common/exceptions.py | 32 ++ .../plugins/sriovnicagent/eswitch_manager.py | 283 ++++++++++++++ neutron/plugins/sriovnicagent/pci_lib.py | 148 +++++++ .../plugins/sriovnicagent/sriov_nic_agent.py | 355 +++++++++++++++++ neutron/tests/unit/sriovnicagent/__init__.py | 0 .../sriovnicagent/test_eswitch_manager.py | 364 ++++++++++++++++++ .../tests/unit/sriovnicagent/test_pci_lib.py | 100 +++++ .../sriovnicagent/test_sriov_agent_config.py | 127 ++++++ .../sriovnicagent/test_sriov_neutron_agent.py | 217 +++++++++++ setup.cfg | 1 + 14 files changed, 1737 insertions(+), 3 deletions(-) create mode 100644 neutron/plugins/sriovnicagent/__init__.py create mode 100644 neutron/plugins/sriovnicagent/common/__init__.py create mode 100644 neutron/plugins/sriovnicagent/common/config.py create mode 100644 neutron/plugins/sriovnicagent/common/exceptions.py create mode 100644 neutron/plugins/sriovnicagent/eswitch_manager.py create mode 100644 neutron/plugins/sriovnicagent/pci_lib.py create mode 100644 neutron/plugins/sriovnicagent/sriov_nic_agent.py create mode 100644 neutron/tests/unit/sriovnicagent/__init__.py create mode 100644 neutron/tests/unit/sriovnicagent/test_eswitch_manager.py create mode 100644 neutron/tests/unit/sriovnicagent/test_pci_lib.py create mode 100644 neutron/tests/unit/sriovnicagent/test_sriov_agent_config.py create mode 100644 neutron/tests/unit/sriovnicagent/test_sriov_neutron_agent.py diff --git a/etc/neutron/plugins/ml2/ml2_conf_sriov.ini b/etc/neutron/plugins/ml2/ml2_conf_sriov.ini index 09504af2d8c..438da7b8fff 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 00000000000..e69de29bb2d diff --git a/neutron/plugins/sriovnicagent/common/__init__.py b/neutron/plugins/sriovnicagent/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/sriovnicagent/common/config.py b/neutron/plugins/sriovnicagent/common/config.py new file mode 100644 index 00000000000..9d8c6a41cd3 --- /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 00000000000..75a781723b4 --- /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 00000000000..b5359077050 --- /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 00000000000..0d615c3ca0b --- /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 00000000000..886269307f2 --- /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 00000000000..e69de29bb2d 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 00000000000..a7c41e3af1d --- /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 00000000000..17862c9185d --- /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 00000000000..d77a9f3224a --- /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 00000000000..ac078e267bf --- /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 2bbcd1a460a..5770756ac38 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