pci.py 11 KB

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