pci.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2016 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. #
  23. import os
  24. import re
  25. import subprocess
  26. import libvirt
  27. import lxml
  28. import lxml.etree
  29. import qubes.devices
  30. import qubes.ext
  31. #: cache of PCI device classes
  32. pci_classes = None
  33. def load_pci_classes():
  34. # List of known device classes, subclasses and programming interfaces
  35. # Syntax:
  36. # C class class_name
  37. # subclass subclass_name <-- single tab
  38. # prog-if prog-if_name <-- two tabs
  39. result = {}
  40. with open('/usr/share/hwdata/pci.ids') as pciids:
  41. class_id = None
  42. subclass_id = None
  43. for line in pciids.readlines():
  44. line = line.rstrip()
  45. if line.startswith('\t\t') and class_id and subclass_id:
  46. (progif_id, _, class_name) = line[2:].split(' ', 2)
  47. result[class_id + subclass_id + progif_id] = \
  48. class_name
  49. elif line.startswith('\t') and class_id:
  50. (subclass_id, _, class_name) = line[1:].split(' ', 2)
  51. # store both prog-if specific entry and generic one
  52. result[class_id + subclass_id + '00'] = \
  53. class_name
  54. result[class_id + subclass_id] = \
  55. class_name
  56. elif line.startswith('C '):
  57. (_, class_id, _, class_name) = line.split(' ', 3)
  58. result[class_id + '0000'] = class_name
  59. result[class_id + '00'] = class_name
  60. subclass_id = None
  61. return result
  62. def pcidev_class(dev_xmldesc):
  63. sysfs_path = dev_xmldesc.findtext('path')
  64. assert sysfs_path
  65. try:
  66. class_id = open(sysfs_path + '/class').read().strip()
  67. except OSError:
  68. return "Unknown"
  69. if not qubes.ext.pci.pci_classes:
  70. qubes.ext.pci.pci_classes = load_pci_classes()
  71. if class_id.startswith('0x'):
  72. class_id = class_id[2:]
  73. try:
  74. # ignore prog-if
  75. return qubes.ext.pci.pci_classes[class_id[0:4]]
  76. except KeyError:
  77. return "Unknown"
  78. def attached_devices(app):
  79. """Return map device->domain-name for all currently attached devices"""
  80. # Libvirt do not expose nice API to query where the device is
  81. # attached. The only way would be to query _all_ the domains (
  82. # each with separate libvirt call) and look if the device is
  83. # there. Horrible waste of resources.
  84. # Instead, do this on much lower level - xenstore info for
  85. # xen-pciback driver, where we get all the info at once
  86. xs = app.vmm.xs
  87. devices = {}
  88. for domid in xs.ls('', 'backend/pci'):
  89. for devid in xs.ls('', 'backend/pci/' + domid):
  90. devpath = 'backend/pci/' + domid + '/' + devid
  91. domain_name = xs.read('', devpath + '/domain')
  92. try:
  93. domain = app.domains[domain_name]
  94. except KeyError:
  95. # unknown domain - maybe from another qubes.xml?
  96. continue
  97. devnum = xs.read('', devpath + '/num_devs')
  98. for dev in range(int(devnum)):
  99. dbdf = xs.read('', devpath + '/dev-' + str(dev))
  100. bdf = dbdf[len('0000:'):]
  101. devices[bdf] = domain
  102. return devices
  103. def _device_desc(hostdev_xml):
  104. return '{devclass}: {vendor} {product}'.format(
  105. devclass=pcidev_class(hostdev_xml),
  106. vendor=hostdev_xml.findtext('capability/vendor'),
  107. product=hostdev_xml.findtext('capability/product'),
  108. )
  109. class PCIDevice(qubes.devices.DeviceInfo):
  110. # pylint: disable=too-few-public-methods
  111. regex = re.compile(
  112. r'^(?P<bus>[0-9a-f]+):(?P<device>[0-9a-f]+)\.(?P<function>[0-9a-f]+)$')
  113. libvirt_regex = re.compile(
  114. r'^pci_0000_(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)_'
  115. r'(?P<function>[0-9a-f]+)$')
  116. def __init__(self, backend_domain, ident, libvirt_name=None):
  117. if libvirt_name:
  118. dev_match = self.libvirt_regex.match(libvirt_name)
  119. assert dev_match
  120. ident = '{bus}:{device}.{function}'.format(**dev_match.groupdict())
  121. super(PCIDevice, self).__init__(backend_domain, ident, None)
  122. # lazy loading
  123. self._description = None
  124. @property
  125. def libvirt_name(self):
  126. # pylint: disable=no-member
  127. # noinspection PyUnresolvedReferences
  128. return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function)
  129. @property
  130. def description(self):
  131. if self._description is None:
  132. hostdev_details = \
  133. self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName(
  134. self.libvirt_name
  135. )
  136. self._description = _device_desc(lxml.etree.fromstring(
  137. hostdev_details.XMLDesc()))
  138. return self._description
  139. @property
  140. def frontend_domain(self):
  141. # TODO: cache this
  142. all_attached = attached_devices(self.backend_domain.app)
  143. return all_attached.get(self.ident, None)
  144. class PCIDeviceExtension(qubes.ext.Extension):
  145. def __init__(self):
  146. super(PCIDeviceExtension, self).__init__()
  147. # lazy load this
  148. self.pci_classes = {}
  149. @qubes.ext.handler('device-list:pci')
  150. def on_device_list_pci(self, vm, event):
  151. # pylint: disable=unused-argument,no-self-use
  152. # only dom0 expose PCI devices
  153. if vm.qid != 0:
  154. return
  155. for dev in vm.app.vmm.libvirt_conn.listAllDevices():
  156. if 'pci' not in dev.listCaps():
  157. continue
  158. xml_desc = lxml.etree.fromstring(dev.XMLDesc())
  159. libvirt_name = xml_desc.findtext('name')
  160. yield PCIDevice(vm, None, libvirt_name=libvirt_name)
  161. @qubes.ext.handler('device-get:pci')
  162. def on_device_get_pci(self, vm, event, ident):
  163. # pylint: disable=unused-argument,no-self-use
  164. if not vm.app.vmm.offline_mode:
  165. yield PCIDevice(vm, ident)
  166. @qubes.ext.handler('device-list-attached:pci')
  167. def on_device_list_attached(self, vm, event, **kwargs):
  168. # pylint: disable=unused-argument,no-self-use
  169. if not vm.is_running() or isinstance(vm, qubes.vm.adminvm.AdminVM):
  170. return
  171. xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
  172. for hostdev in xml_desc.findall('devices/hostdev'):
  173. if hostdev.get('type') != 'pci':
  174. continue
  175. address = hostdev.find('source/address')
  176. bus = address.get('bus')[2:]
  177. device = address.get('slot')[2:]
  178. function = address.get('function')[2:]
  179. ident = '{bus}:{device}.{function}'.format(
  180. bus=bus,
  181. device=device,
  182. function=function,
  183. )
  184. yield PCIDevice(vm.app.domains[0], ident)
  185. @qubes.ext.handler('device-pre-attach:pci')
  186. def on_device_pre_attached_pci(self, vm, event, device):
  187. # pylint: disable=unused-argument
  188. if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(
  189. device.ident)):
  190. raise qubes.exc.QubesException(
  191. 'Invalid PCI device: {}'.format(device.ident))
  192. if not vm.is_running():
  193. return
  194. try:
  195. self.bind_pci_to_pciback(vm.app, device)
  196. vm.libvirt_domain.attachDevice(
  197. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  198. device=device))
  199. except subprocess.CalledProcessError as e:
  200. vm.log.exception('Failed to attach PCI device {!r} on the fly,'
  201. ' changes will be seen after VM restart.'.format(
  202. device.ident), e)
  203. @qubes.ext.handler('device-pre-detach:pci')
  204. def on_device_pre_detached_pci(self, vm, event, device):
  205. # pylint: disable=unused-argument,no-self-use
  206. if not vm.is_running():
  207. return
  208. # this cannot be converted to general API, because there is no
  209. # provision in libvirt for extracting device-side BDF; we need it for
  210. # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel
  211. p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)],
  212. stdout=subprocess.PIPE)
  213. result = p.communicate()[0]
  214. m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident), result,
  215. flags=re.MULTILINE)
  216. if not m:
  217. vm.log.error('Device %s already detached', device.ident)
  218. return
  219. vmdev = m.group(1)
  220. try:
  221. vm.run_service('qubes.DetachPciDevice',
  222. user='root', input='00:{}'.format(vmdev))
  223. vm.libvirt_domain.detachDevice(
  224. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  225. device=device))
  226. except (subprocess.CalledProcessError, libvirt.libvirtError) as e:
  227. vm.log.exception('Failed to detach PCI device {!r} on the fly,'
  228. ' changes will be seen after VM restart.'.format(
  229. device.ident), e)
  230. raise
  231. @qubes.ext.handler('domain-pre-start')
  232. def on_domain_pre_start(self, vm, _event, **kwargs):
  233. # Bind pci devices to pciback driver
  234. for pci in vm.devices['pci'].attached():
  235. self.bind_pci_to_pciback(vm.app, pci)
  236. @staticmethod
  237. def bind_pci_to_pciback(app, device):
  238. '''Bind PCI device to pciback driver.
  239. :param qubes.devices.PCIDevice device: device to attach
  240. Devices should be unbound from their normal kernel drivers and bound to
  241. the dummy driver, which allows for attaching them to a domain.
  242. '''
  243. try:
  244. node = app.vmm.libvirt_conn.nodeDeviceLookupByName(
  245. device.libvirt_name)
  246. except libvirt.libvirtError as e:
  247. if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
  248. raise qubes.exc.QubesException(
  249. 'PCI device {!r} does not exist'.format(
  250. device))
  251. raise
  252. try:
  253. node.dettach()
  254. except libvirt.libvirtError as e:
  255. if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
  256. # allreaddy dettached
  257. pass
  258. else:
  259. raise