Browse Source

qubes/ext/pci: move PCI devices handling to an extension

Implement required event handlers according to documentation in
qubes.devices.

A modification of qubes.devices.DeviceInfo is needed to allow dynamic,
read-only properties.

QubesOS/qubes-issues#2257
Marek Marczykowski-Górecki 7 years ago
parent
commit
aa67a4512e
7 changed files with 329 additions and 89 deletions
  1. 11 4
      qubes/devices.py
  2. 304 0
      qubes/ext/pci.py
  3. 8 1
      qubes/tests/vm/init.py
  4. 1 81
      qubes/vm/qubesvm.py
  5. 1 0
      rpm_spec/core-dom0.spec
  6. 2 1
      setup.py
  7. 2 2
      templates/libvirt/xen.xml

+ 11 - 4
qubes/devices.py

@@ -245,10 +245,17 @@ class DeviceInfo(object):
         self.backend_domain = backend_domain
         #: device identifier (unique for given domain and device type)
         self.ident = ident
-        #: human readable description/name of the device
-        self.description = description
-        #: (running) domain to which device is currently attached
-        self.frontend_domain = frontend_domain
+        # allow redefining those as dynamic properties in subclasses
+        try:
+            #: human readable description/name of the device
+            self.description = description
+        except AttributeError:
+            pass
+        try:
+            #: (running) domain to which device is currently attached
+            self.frontend_domain = frontend_domain
+        except AttributeError:
+            pass
         self.data = kwargs
 
         if hasattr(self, 'regex'):

+ 304 - 0
qubes/ext/pci.py

@@ -0,0 +1,304 @@
+#!/usr/bin/python2 -O
+# vim: fileencoding=utf-8
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2016  Marek Marczykowski-Górecki
+#                                   <marmarek@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+import os
+import re
+import subprocess
+import libvirt
+import lxml
+import lxml.etree
+
+import qubes.devices
+import qubes.ext
+
+#: cache of PCI device classes
+pci_classes = None
+
+
+def load_pci_classes():
+    # List of known device classes, subclasses and programming interfaces
+    # Syntax:
+    # C class       class_name
+    #       subclass        subclass_name           <-- single tab
+    #               prog-if  prog-if_name   <-- two tabs
+    result = {}
+    with open('/usr/share/hwdata/pci.ids') as pciids:
+        class_id = None
+        subclass_id = None
+        for line in pciids.readlines():
+            line = line.rstrip()
+            if line.startswith('\t\t') and class_id and subclass_id:
+                (progif_id, _, class_name) = line[2:].split(' ', 2)
+                result[class_id + subclass_id + progif_id] = \
+                    class_name
+            elif line.startswith('\t') and class_id:
+                (subclass_id, _, class_name) = line[1:].split(' ', 2)
+                # store both prog-if specific entry and generic one
+                result[class_id + subclass_id + '00'] = \
+                    class_name
+                result[class_id + subclass_id] = \
+                    class_name
+            elif line.startswith('C '):
+                (_, class_id, _, class_name) = line.split(' ', 3)
+                result[class_id + '0000'] = class_name
+                result[class_id + '00'] = class_name
+                subclass_id = None
+
+    return result
+
+
+def pcidev_class(dev_xmldesc):
+    sysfs_path = dev_xmldesc.findtext('path')
+    assert sysfs_path
+    try:
+        class_id = open(sysfs_path + '/class').read().strip()
+    except OSError:
+        return "Unknown"
+
+    if not qubes.ext.pci.pci_classes:
+        qubes.ext.pci.pci_classes = load_pci_classes()
+    if class_id.startswith('0x'):
+        class_id = class_id[2:]
+    try:
+        # ignore prog-if
+        return qubes.ext.pci.pci_classes[class_id[0:4]]
+    except KeyError:
+        return "Unknown"
+
+
+def attached_devices(app):
+    """Return map device->domain-name for all currently attached devices"""
+
+    # Libvirt do not expose nice API to query where the device is
+    # attached. The only way would be to query _all_ the domains (
+    # each with separate libvirt call) and look if the device is
+    # there. Horrible waste of resources.
+    # Instead, do this on much lower level - xenstore info for
+    # xen-pciback driver, where we get all the info at once
+
+    xs = app.vmm.xs
+    devices = {}
+    for domid in xs.ls('', 'backend/pci'):
+        for devid in xs.ls('', 'backend/pci/' + domid):
+            devpath = 'backend/pci/' + domid + '/' + devid
+            domain_name = xs.read('', devpath + '/domain')
+            try:
+                domain = app.domains[domain_name]
+            except KeyError:
+                # unknown domain - maybe from another qubes.xml?
+                continue
+            devnum = xs.read('', devpath + '/num_devs')
+            for dev in range(int(devnum)):
+                dbdf = xs.read('', devpath + '/dev-' + str(dev))
+                bdf = dbdf[len('0000:'):]
+                devices[bdf] = domain
+
+    return devices
+
+
+def _device_desc(hostdev_xml):
+    return '{devclass}: {vendor} {product}'.format(
+        devclass=pcidev_class(hostdev_xml),
+        vendor=hostdev_xml.findtext('capability/vendor'),
+        product=hostdev_xml.findtext('capability/product'),
+    )
+
+
+
+class PCIDevice(qubes.devices.DeviceInfo):
+    # pylint: disable=too-few-public-methods
+    regex = re.compile(
+        r'^(?P<bus>[0-9a-f]+):(?P<device>[0-9a-f]+)\.(?P<function>[0-9a-f]+)$')
+    libvirt_regex = re.compile(
+        r'^pci_0000_(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)_'
+        r'(?P<function>[0-9a-f]+)$')
+
+    def __init__(self, backend_domain, ident, libvirt_name=None):
+        if libvirt_name:
+            dev_match = self.libvirt_regex.match(libvirt_name)
+            assert dev_match
+            ident = '{bus}:{device}.{function}'.format(**dev_match.groupdict())
+
+        super(PCIDevice, self).__init__(backend_domain, ident, None)
+
+        # lazy loading
+        self._description = None
+
+    @property
+    def libvirt_name(self):
+        # pylint: disable=no-member
+        # noinspection PyUnresolvedReferences
+        return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function)
+
+    @property
+    def description(self):
+        if self._description is None:
+            hostdev_details = \
+                self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName(
+                    self.libvirt_name
+                )
+            self._description = _device_desc(lxml.etree.fromstring(
+                hostdev_details.XMLDesc()))
+        return self._description
+
+    @property
+    def frontend_domain(self):
+        # TODO: cache this
+        all_attached = attached_devices(self.backend_domain.app)
+        return all_attached.get(self.ident, None)
+
+
+class PCIDeviceExtension(qubes.ext.Extension):
+    def __init__(self):
+        super(PCIDeviceExtension, self).__init__()
+        # lazy load this
+        self.pci_classes = {}
+
+    @qubes.ext.handler('device-list:pci')
+    def on_device_list_pci(self, vm, event):
+        # pylint: disable=unused-argument,no-self-use
+        # only dom0 expose PCI devices
+        if vm.qid != 0:
+            return
+
+        for dev in vm.app.vmm.libvirt_conn.listAllDevices():
+            if 'pci' not in dev.listCaps():
+                continue
+
+            xml_desc = lxml.etree.fromstring(dev.XMLDesc())
+            libvirt_name = xml_desc.findtext('name')
+            yield PCIDevice(vm, None, libvirt_name=libvirt_name)
+
+    @qubes.ext.handler('device-get:pci')
+    def on_device_get_pci(self, vm, event, ident):
+        # pylint: disable=unused-argument,no-self-use
+        if not vm.app.vmm.offline_mode:
+            yield PCIDevice(vm, ident)
+
+    @qubes.ext.handler('device-list-attached:pci')
+    def on_device_list_attached(self, vm, event, **kwargs):
+        # pylint: disable=unused-argument,no-self-use
+        if not vm.is_running() or isinstance(vm, qubes.vm.adminvm.AdminVM):
+            return
+        xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
+
+        for hostdev in xml_desc.findall('devices/hostdev'):
+            if hostdev.get('type') != 'pci':
+                continue
+            address = hostdev.find('source/address')
+            bus = address.get('bus')[2:]
+            device = address.get('slot')[2:]
+            function = address.get('function')[2:]
+
+            ident = '{bus}:{device}.{function}'.format(
+                bus=bus,
+                device=device,
+                function=function,
+            )
+            yield PCIDevice(vm.app.domains[0], ident)
+
+    @qubes.ext.handler('device-pre-attach:pci')
+    def on_device_pre_attached_pci(self, vm, event, device):
+        # pylint: disable=unused-argument
+        if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(
+                device.ident)):
+            raise qubes.exc.QubesException(
+                'Invalid PCI device: {}'.format(device.ident))
+
+        if not vm.is_running():
+            return
+
+        try:
+            self.bind_pci_to_pciback(vm.app, device)
+            vm.libvirt_domain.attachDevice(
+                vm.app.env.get_template('libvirt/devices/pci.xml').render(
+                    device=device))
+        except subprocess.CalledProcessError as e:
+            vm.log.exception('Failed to attach PCI device {!r} on the fly,'
+                ' changes will be seen after VM restart.'.format(
+                device.ident), e)
+
+    @qubes.ext.handler('device-pre-detach:pci')
+    def on_device_pre_detached_pci(self, vm, event, device):
+        # pylint: disable=unused-argument,no-self-use
+        if not vm.is_running():
+            return
+
+        # this cannot be converted to general API, because there is no
+        # provision in libvirt for extracting device-side BDF; we need it for
+        # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel
+
+        p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)],
+                stdout=subprocess.PIPE)
+        result = p.communicate()[0]
+        m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident), result,
+            flags=re.MULTILINE)
+        if not m:
+            vm.log.error('Device %s already detached', device.ident)
+            return
+        vmdev = m.group(1)
+        try:
+            vm.run_service('qubes.DetachPciDevice',
+                user='root', input='00:{}'.format(vmdev))
+            vm.libvirt_domain.detachDevice(
+                vm.app.env.get_template('libvirt/devices/pci.xml').render(
+                    device=device))
+        except (subprocess.CalledProcessError, libvirt.libvirtError) as e:
+            vm.log.exception('Failed to detach PCI device {!r} on the fly,'
+                ' changes will be seen after VM restart.'.format(
+                device.ident), e)
+            raise
+
+    @qubes.ext.handler('domain-pre-start')
+    def on_domain_pre_start(self, vm, _event, **kwargs):
+        # Bind pci devices to pciback driver
+        for pci in vm.devices['pci'].attached():
+            self.bind_pci_to_pciback(vm.app, pci)
+
+    @staticmethod
+    def bind_pci_to_pciback(app, device):
+        '''Bind PCI device to pciback driver.
+
+        :param qubes.devices.PCIDevice device: device to attach
+
+        Devices should be unbound from their normal kernel drivers and bound to
+        the dummy driver, which allows for attaching them to a domain.
+        '''
+        try:
+            node = app.vmm.libvirt_conn.nodeDeviceLookupByName(
+                device.libvirt_name)
+        except libvirt.libvirtError as e:
+            if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
+                raise qubes.exc.QubesException(
+                    'PCI device {!r} does not exist'.format(
+                        device))
+            raise
+
+        try:
+            node.dettach()
+        except libvirt.libvirtError as e:
+            if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
+                # allreaddy dettached
+                pass
+            else:
+                raise
+

+ 8 - 1
qubes/tests/vm/init.py

@@ -31,10 +31,17 @@ import qubes.vm
 
 import qubes.tests
 
+class TestVMM(object):
+    def __init__(self):
+        super(TestVMM, self).__init__()
+        self.offline_mode = True
+
+
 class TestApp(object):
     def __init__(self):
         super(TestApp, self).__init__()
         self.domains = {}
+        self.vmm = TestVMM()
 
 
 class TestVM(qubes.vm.BaseVM):
@@ -104,7 +111,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase):
 
         self.assertItemsEqual(vm.devices.keys(), ('pci',))
         self.assertItemsEqual(list(vm.devices['pci'].attached(persistent=True)),
-            [qubes.devices.PCIDevice(vm, '00:11.22')])
+            [qubes.ext.pci.PCIDevice(vm, '00:11.22')])
 
         self.assertXMLIsValid(vm.__xml__(), 'domain.rng')
 

+ 1 - 81
qubes/vm/qubesvm.py

@@ -243,7 +243,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
     # CORE2: swallowed uses_default_kernelopts
     kernelopts = qubes.property('kernelopts', type=str, load_stage=4,
         default=(lambda self: qubes.config.defaults['kernelopts_pcidevs']
-            if list(self.devices['pci'].attached())
+            if list(self.devices['pci'].attached(persistent=True))
             else self.template.kernelopts if hasattr(self, 'template')
             else qubes.config.defaults['kernelopts']),
         ls_width=30,
@@ -588,82 +588,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
                 raise qubes.exc.QubesException(
                     'Failed to reset autostart for VM in systemd')
 
-    @qubes.events.handler('device-pre-attach:pci')
-    def on_device_pre_attached_pci(self, event, device):
-        # pylint: disable=unused-argument
-        if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(device)):
-            raise qubes.exc.QubesException(
-                'Invalid PCI device: {}'.format(device))
-
-        if not self.is_running():
-            return
-
-        try:
-            self.bind_pci_to_pciback(device)
-            self.libvirt_domain.attachDevice(
-                self.app.env.get_template('libvirt/devices/pci.xml').render(
-                    device=device))
-        except subprocess.CalledProcessError as e:
-            self.log.exception('Failed to attach PCI device {!r} on the fly,'
-                ' changes will be seen after VM restart.'.format(device), e)
-
-    @qubes.events.handler('device-pre-detach:pci')
-    def on_device_pre_detached_pci(self, event, device):
-        # pylint: disable=unused-argument
-        if not self.is_running():
-            return
-
-        # this cannot be converted to general API, because there is no
-        # provision in libvirt for extracting device-side BDF; we need it for
-        # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel
-
-        p = subprocess.Popen(['xl', 'pci-list', str(self.xid)],
-                stdout=subprocess.PIPE)
-        result = p.communicate()[0]
-        m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device), result,
-            flags=re.MULTILINE)
-        if not m:
-            self.log.error('Device %s already detached', device)
-            return
-        vmdev = m.group(1)
-        try:
-            self.run_service('qubes.DetachPciDevice',
-                user='root', input='00:{}'.format(vmdev))
-            self.libvirt_domain.detachDevice(
-                self.app.env.get_template('libvirt/devices/pci.xml').render(
-                    device=device))
-        except (subprocess.CalledProcessError, libvirt.libvirtError) as e:
-            self.log.exception('Failed to detach PCI device {!r} on the fly,'
-                ' changes will be seen after VM restart.'.format(device), e)
-            raise
-
-    def bind_pci_to_pciback(self, device):
-        '''Bind PCI device to pciback driver.
-
-        :param qubes.devices.PCIDevice device: device to attach
-
-        Devices should be unbound from their normal kernel drivers and bound to
-        the dummy driver, which allows for attaching them to a domain.
-        '''
-        try:
-            node = self.app.vmm.libvirt_conn.nodeDeviceLookupByName(
-                device.libvirt_name)
-        except libvirt.libvirtError as e:
-            if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
-                raise qubes.exc.QubesException(
-                    'PCI device {!r} does not exist (domain {!r})'.format(
-                        device, self.name))
-            raise
-
-        try:
-            node.dettach()
-        except libvirt.libvirtError as e:
-            if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
-                # allreaddy dettached
-                pass
-            else:
-                raise
-
     #
     # methods for changing domain state
     #
@@ -702,10 +626,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
 
         qmemman_client = self.request_memory(mem_required)
 
-        # Bind pci devices to pciback driver
-        for pci in self.devices['pci'].attached():
-            self.bind_pci_to_pciback(pci)
-
         self.libvirt_domain.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED)
 
         try:

+ 1 - 0
rpm_spec/core-dom0.spec

@@ -268,6 +268,7 @@ fi
 %dir %{python_sitelib}/qubes/ext
 %{python_sitelib}/qubes/ext/__init__.py*
 %{python_sitelib}/qubes/ext/gui.py*
+%{python_sitelib}/qubes/ext/pci.py*
 %{python_sitelib}/qubes/ext/qubesmanager.py*
 %{python_sitelib}/qubes/ext/r3compatibility.py*
 

+ 2 - 1
setup.py

@@ -40,9 +40,10 @@ if __name__ == '__main__':
                 'qubes.ext.qubesmanager = qubes.ext.qubesmanager:QubesManager',
                 'qubes.ext.gui = qubes.ext.gui:GUI',
                 'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility',
+                'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
             ],
             'qubes.devices': [
-                'pci = qubes.devices:PCIDevice',
+                'pci = qubes.ext.pci:PCIDevice',
                 'testclass = qubes.tests.devices:TestDevice',
             ],
             'qubes.storage': [

+ 2 - 2
templates/libvirt/xen.xml

@@ -27,7 +27,7 @@
             <viridian/>
         {% endif %}
 
-        {% if vm.devices['pci'].attached() | list
+        {% if vm.devices['pci'].attached(persistent=True) | list
                 and vm.features.get('pci-e820-host', True) %}
             <xen>
                 <e820_host state="on"/>
@@ -94,7 +94,7 @@
             {% include 'libvirt/devices/net.xml' with context %}
         {% endif %}
 
-        {% for device in vm.devices.pci.attached() %}
+        {% for device in vm.devices.pci.attached(persistent=True) %}
             {% include 'libvirt/devices/pci.xml' %}
         {% endfor %}