pci.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  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. with open(sysfs_path + '/class') as f_class:
  66. class_id = f_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') or []:
  89. for devid in xs.ls('', 'backend/pci/' + domid) or []:
  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.replace(':', '_')] = 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 _cache_get(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, options):
  187. # pylint: disable=unused-argument
  188. if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(
  189. device.ident.replace('_', ':'))):
  190. raise qubes.exc.QubesException(
  191. 'Invalid PCI device: {}'.format(device.ident))
  192. if vm.virt_mode == 'pvh':
  193. raise qubes.exc.QubesException(
  194. "Can't attach PCI device to VM in pvh mode")
  195. if not vm.is_running():
  196. return
  197. try:
  198. device = _cache_get(vm, device.ident)
  199. self.bind_pci_to_pciback(vm.app, device)
  200. vm.libvirt_domain.attachDevice(
  201. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  202. device=device, vm=vm, options=options))
  203. except subprocess.CalledProcessError as e:
  204. vm.log.exception('Failed to attach PCI device {!r} on the fly,'
  205. ' changes will be seen after VM restart.'.format(
  206. device.ident), e)
  207. @qubes.ext.handler('device-pre-detach:pci')
  208. def on_device_pre_detached_pci(self, vm, event, device):
  209. # pylint: disable=unused-argument,no-self-use
  210. if not vm.is_running():
  211. return
  212. # this cannot be converted to general API, because there is no
  213. # provision in libvirt for extracting device-side BDF; we need it for
  214. # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel
  215. device = _cache_get(vm, device.ident)
  216. p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)],
  217. stdout=subprocess.PIPE)
  218. result = p.communicate()[0].decode()
  219. m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident.replace(
  220. '_', ':')),
  221. result,
  222. flags=re.MULTILINE)
  223. if not m:
  224. vm.log.error('Device %s already detached', device.ident)
  225. return
  226. vmdev = m.group(1)
  227. try:
  228. vm.run_service('qubes.DetachPciDevice',
  229. user='root', input='00:{}'.format(vmdev))
  230. vm.libvirt_domain.detachDevice(
  231. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  232. device=device, vm=vm))
  233. except (subprocess.CalledProcessError, libvirt.libvirtError) as e:
  234. vm.log.exception('Failed to detach PCI device {!r} on the fly,'
  235. ' changes will be seen after VM restart.'.format(
  236. device.ident), e)
  237. raise
  238. @qubes.ext.handler('domain-pre-start')
  239. def on_domain_pre_start(self, vm, _event, **_kwargs):
  240. # Bind pci devices to pciback driver
  241. for assignment in vm.devices['pci'].persistent():
  242. device = _cache_get(vm, assignment.ident)
  243. self.bind_pci_to_pciback(vm.app, device)
  244. @staticmethod
  245. def bind_pci_to_pciback(app, device):
  246. '''Bind PCI device to pciback driver.
  247. :param qubes.devices.PCIDevice device: device to attach
  248. Devices should be unbound from their normal kernel drivers and bound to
  249. the dummy driver, which allows for attaching them to a domain.
  250. '''
  251. try:
  252. node = app.vmm.libvirt_conn.nodeDeviceLookupByName(
  253. device.libvirt_name)
  254. except libvirt.libvirtError as e:
  255. if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
  256. raise qubes.exc.QubesException(
  257. 'PCI device {!r} does not exist'.format(
  258. device))
  259. raise
  260. try:
  261. node.dettach()
  262. except libvirt.libvirtError as e:
  263. if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
  264. # allreaddy dettached
  265. pass
  266. else:
  267. raise
  268. @functools.lru_cache(maxsize=None)
  269. def _cache_get(vm, ident):
  270. ''' Caching wrapper around `PCIDevice(vm, ident)`. '''
  271. return PCIDevice(vm, ident)