diff --git a/nova/tests/virt/xenapi/test_driver.py b/nova/tests/virt/xenapi/test_driver.py index 77696940c8ff..81bedf5ae9a2 100644 --- a/nova/tests/virt/xenapi/test_driver.py +++ b/nova/tests/virt/xenapi/test_driver.py @@ -33,7 +33,8 @@ class XenAPIDriverTestCase(stubs.XenAPITestBaseNoDB): 'host_hostname': 'somename', 'supported_instances': 'x86_64', 'host_cpu_info': {'cpu_count': 50}, - 'vcpus_used': 10} + 'vcpus_used': 10, + 'pci_passthrough_devices': ''} def test_available_resource(self): self.flags(connection_url='test_url', diff --git a/nova/tests/virt/xenapi/test_xenapi.py b/nova/tests/virt/xenapi/test_xenapi.py index d9309699296a..05174548561e 100644 --- a/nova/tests/virt/xenapi/test_xenapi.py +++ b/nova/tests/virt/xenapi/test_xenapi.py @@ -2034,6 +2034,18 @@ class XenAPIHostTestCase(stubs.XenAPITestBase): stats = self.conn.get_host_stats(True) self.assertEqual(stats['vcpus_used'], 4) + def test_pci_passthrough_devices_whitelist(self): + # NOTE(guillaume-thouvenin): This pci whitelist will be used to + # match with _plugin_xenhost_get_pci_device_details method in fake.py. + self.flags(pci_passthrough_whitelist= + ['[{"vendor_id":"10de", "product_id":"11bf"}]']) + stats = self.conn.get_host_stats() + self.assertEqual(len(stats['pci_passthrough_devices']), 1) + + def test_pci_passthrough_devices_no_whitelist(self): + stats = self.conn.get_host_stats() + self.assertEqual(len(stats['pci_passthrough_devices']), 0) + def test_host_state_missing_sr(self): def fake_safe_find_sr(session): raise exception.StorageRepositoryNotFound('not there') diff --git a/nova/virt/xenapi/client/session.py b/nova/virt/xenapi/client/session.py index 97f2846e11ac..1797213ffd24 100644 --- a/nova/virt/xenapi/client/session.py +++ b/nova/virt/xenapi/client/session.py @@ -59,7 +59,7 @@ class XenAPISession(object): # changed in development environments. # MAJOR VERSION: Incompatible changes with the plugins # MINOR VERSION: Compatible changes, new plguins, etc - PLUGIN_REQUIRED_VERSION = '1.1' + PLUGIN_REQUIRED_VERSION = '1.2' def __init__(self, url, user, pw): import XenAPI diff --git a/nova/virt/xenapi/driver.py b/nova/virt/xenapi/driver.py index ff6e656eda84..31ddd8fd6fb3 100644 --- a/nova/virt/xenapi/driver.py +++ b/nova/virt/xenapi/driver.py @@ -482,7 +482,9 @@ class XenAPIDriver(driver.ComputeDriver): # arch_filter.py - see libvirt/driver.py get_cpu_info 'cpu_info': jsonutils.dumps(host_stats['host_cpu_info']), 'supported_instances': jsonutils.dumps( - host_stats['supported_instances'])} + host_stats['supported_instances']), + 'pci_passthrough_devices': jsonutils.dumps( + host_stats['pci_passthrough_devices'])} return dic diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index ef70cf145efd..9508f92d2b30 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -689,6 +689,33 @@ class SessionBase(object): def _plugin_xenhost_host_uptime(self, method, args): return jsonutils.dumps({"uptime": "fake uptime"}) + def _plugin_xenhost_get_pci_device_details(self, method, args): + """Simulate the ouput of three pci devices. + + Both of those devices are available for pci passtrough but + only one will match with the pci whitelist used in the + method test_pci_passthrough_devices_*(). + Return a single list. + + """ + # Driver is not pciback + dev_bad1 = ["Slot:\t86:10.0", "Class:\t0604", "Vendor:\t10b5", + "Device:\t8747", "Rev:\tba", "Driver:\tpcieport", "\n"] + # Driver is pciback but vendor and device are bad + dev_bad2 = ["Slot:\t88:00.0", "Class:\t0300", "Vendor:\t0bad", + "Device:\tcafe", "SVendor:\t10de", "SDevice:\t100d", + "Rev:\ta1", "Driver:\tpciback", "\n"] + # Driver is pciback and vendor, device are used for matching + dev_good = ["Slot:\t87:00.0", "Class:\t0300", "Vendor:\t10de", + "Device:\t11bf", "SVendor:\t10de", "SDevice:\t100d", + "Rev:\ta1", "Driver:\tpciback", "\n"] + + lspci_output = "\n".join(dev_bad1 + dev_bad2 + dev_good) + return pickle.dumps(lspci_output) + + def _plugin_xenhost_get_pci_type(self, method, args): + return pickle.dumps("type-PCI") + def _plugin_console_get_console_log(self, method, args): dom_id = args["dom_id"] if dom_id == 0: @@ -696,7 +723,7 @@ class SessionBase(object): return base64.b64encode(zlib.compress("dom_id: %s" % dom_id)) def _plugin_nova_plugin_version_get_version(self, method, args): - return pickle.dumps("1.1") + return pickle.dumps("1.2") def _plugin_xenhost_query_gc(self, method, args): return pickle.dumps("False") diff --git a/nova/virt/xenapi/host.py b/nova/virt/xenapi/host.py index 128e10aaab5e..d7ebcb013801 100644 --- a/nova/virt/xenapi/host.py +++ b/nova/virt/xenapi/host.py @@ -17,6 +17,8 @@ Management class for host-related functions (start, reboot, etc). """ +import re + from nova.compute import task_states from nova.compute import vm_states from nova import conductor @@ -27,6 +29,7 @@ from nova.objects import instance as instance_obj from nova.openstack.common.gettextutils import _ from nova.openstack.common import jsonutils from nova.openstack.common import log as logging +from nova.pci import pci_whitelist from nova.virt.xenapi import pool_states from nova.virt.xenapi import vm_utils @@ -149,8 +152,84 @@ class HostState(object): super(HostState, self).__init__() self._session = session self._stats = {} + self._pci_device_filter = pci_whitelist.get_pci_devices_filter() self.update_status() + def _get_passthrough_devices(self): + """Get a list pci devices that are available for pci passthtough. + + We use a plugin to get the output of the lspci command runs on dom0. + From this list we will extract pci devices that are using the pciback + kernel driver. Then we compare this list to the pci whitelist to get + a new list of pci devices that can be used for pci passthrough. + + :returns: a list of pci devices available for pci passthrough. + """ + def _compile_hex(pattern): + """ + Return a compiled regular expression pattern into which we have + replaced occurences of hex by [\da-fA-F]. + """ + return re.compile(pattern.replace("hex", r"[\da-fA-F]")) + + def _parse_pci_device_string(dev_string): + """ + Exctract information from the device string about the slot, the + vendor and the product ID. The string is as follow: + "Slot:\tBDF\nClass:\txxxx\nVendor:\txxxx\nDevice:\txxxx\n..." + Return a dictionary with informations about the device. + """ + slot_regex = _compile_hex(r"Slot:\t" + r"((?:hex{4}:)?" # Domain: (optional) + r"hex{2}:" # Bus: + r"hex{2}\." # Device. + r"hex{1})") # Function + vendor_regex = _compile_hex(r"\nVendor:\t(hex+)") + product_regex = _compile_hex(r"\nDevice:\t(hex+)") + + slot_id = slot_regex.findall(dev_string) + vendor_id = vendor_regex.findall(dev_string) + product_id = product_regex.findall(dev_string) + + if not slot_id or not vendor_id or not product_id: + raise exception.NovaException( + _("Failed to parse information about" + " a pci device for passthrough")) + + type_pci = self._session.call_plugin_serialized( + 'xenhost', 'get_pci_type', slot_id[0]) + + return {'label': '_'.join(['label', + vendor_id[0], + product_id[0]]), + 'vendor_id': vendor_id[0], + 'product_id': product_id[0], + 'address': slot_id[0], + 'dev_id': '_'.join(['pci', slot_id[0]]), + 'dev_type': type_pci, + 'status': 'available'} + + # Devices are separated by a blank line. That is why we + # use "\n\n" as separator. + lspci_out = self._session.call_plugin_serialized( + 'xenhost', 'get_pci_device_details') + pci_list = lspci_out.split("\n\n") + + # For each device of the list, check if it uses the pciback + # kernel driver and if it does, get informations and add it + # to the list of passthrough_devices. Ignore it if the driver + # is not pciback. + passthrough_devices = [] + + for dev_string_info in pci_list: + if "Driver:\tpciback" in dev_string_info: + new_dev = _parse_pci_device_string(dev_string_info) + + if self._pci_device_filter.device_assignable(new_dev): + passthrough_devices.append(new_dev) + + return passthrough_devices + def get_host_stats(self, refresh=False): """Return the current state of the host. If 'refresh' is True, run the update first. @@ -196,6 +275,7 @@ class HostState(object): for vm_ref, vm_rec in vm_utils.list_vms(self._session): vcpus_used = vcpus_used + int(vm_rec['VCPUs_max']) data['vcpus_used'] = vcpus_used + data['pci_passthrough_devices'] = self._get_passthrough_devices() self._stats = data diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version index 921847bc97d9..12a58bcb23b0 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/nova_plugin_version @@ -24,7 +24,8 @@ import utils # 1.0 - Initial version. # 1.1 - New call to check GC status -PLUGIN_VERSION = "1.1" +# 1.2 - Added support for pci passthrough devices +PLUGIN_VERSION = "1.2" def get_version(session): return PLUGIN_VERSION diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost index e62664480a55..b51d6db92831 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/xenhost @@ -27,8 +27,8 @@ except ImportError: import simplejson as json import logging import re -import time import sys +import time import xmlrpclib import utils @@ -403,11 +403,42 @@ def query_gc(session, sr_uuid, vdi_uuid): # Example output: "Currently running: True" return result[19:].strip() == "True" +def get_pci_device_details(session): + """Returns a string that is a list of pci devices with details. + + This string is obtained by running the command lspci. With -vmm option, + it dumps PCI device data in machine readable form. This verbose format + display a sequence of records separated by a blank line. We will also + use option "-n" to get vendor_id and device_id as numeric values and + the "-k" option to get the kernel driver used if any. + """ + return _run_command(["lspci", "-vmmnk"]) + + +def get_pci_type(session, pci_device): + """Returns the type of the PCI device (type-PCI, type-VF or type-PF). + + pci-device -- The address of the pci device + """ + # We need to add the domain if it is missing + if pci_device.count(':') == 1: + pci_device = "0000:" + pci_device + output = _run_command(["ls", "/sys/bus/pci/devices/" + pci_device + "/"]) + + if "physfn" in output: + return "type-VF" + if "virtfn" in output: + return "type-PF" + return "type-PCI" + + if __name__ == "__main__": # Support both serialized and non-serialized plugin approaches _, methodname = xmlrpclib.loads(sys.argv[1]) - if methodname in ['query_gc']: - utils.register_plugin_calls(query_gc) + if methodname in ['query_gc', 'get_pci_device_details', 'get_pci_type']: + utils.register_plugin_calls(query_gc, + get_pci_device_details, + get_pci_type) XenAPIPlugin.dispatch( {"host_data": host_data,