devices.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2015-2016 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  7. # Copyright (C) 2017 Marek Marczykowski-Górecki
  8. # <marmarek@invisiblethingslab.com>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU Lesser General Public License as published by
  12. # the Free Software Foundation; either version 2.1 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU Lesser General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU Lesser General Public License along
  21. # with this program; if not, see <http://www.gnu.org/licenses/>.
  22. """API for various types of devices.
  23. Main concept is that some domain main
  24. expose (potentially multiple) devices, which can be attached to other domains.
  25. Devices can be of different classes (like 'pci', 'usb', etc). Each device
  26. class is implemented by an extension.
  27. Devices are identified by pair of (backend domain, `ident`), where `ident` is
  28. :py:class:`str`.
  29. """
  30. class DeviceAssignment(object): # pylint: disable=too-few-public-methods
  31. """ Maps a device to a frontend_domain. """
  32. def __init__(self, backend_domain, ident, options=None, persistent=False,
  33. frontend_domain=None, devclass=None):
  34. self.backend_domain = backend_domain
  35. self.ident = ident
  36. self.devclass = devclass
  37. self.options = options or {}
  38. self.persistent = persistent
  39. self.frontend_domain = frontend_domain
  40. def __repr__(self):
  41. return "[%s]:%s" % (self.backend_domain, self.ident)
  42. def __hash__(self):
  43. return hash((self.backend_domain, self.ident))
  44. def __eq__(self, other):
  45. if not isinstance(self, other.__class__):
  46. return NotImplemented
  47. return self.backend_domain == other.backend_domain \
  48. and self.ident == other.ident
  49. def clone(self):
  50. """Clone object instance"""
  51. return self.__class__(
  52. self.backend_domain,
  53. self.ident,
  54. self.options,
  55. self.persistent,
  56. self.frontend_domain,
  57. self.devclass,
  58. )
  59. @property
  60. def device(self):
  61. """Get DeviceInfo object corresponding to this DeviceAssignment"""
  62. return self.backend_domain.devices[self.devclass][self.ident]
  63. class DeviceInfo(object):
  64. """ Holds all information about a device """
  65. # pylint: disable=too-few-public-methods
  66. def __init__(self, backend_domain, devclass, ident, description=None,
  67. **kwargs):
  68. #: domain providing this device
  69. self.backend_domain = backend_domain
  70. #: device class
  71. self.devclass = devclass
  72. #: device identifier (unique for given domain and device type)
  73. self.ident = ident
  74. #: human readable description/name of the device
  75. self.description = description
  76. self.data = kwargs
  77. def __hash__(self):
  78. return hash((str(self.backend_domain), self.ident))
  79. def __eq__(self, other):
  80. try:
  81. return (
  82. self.devclass == other.devclass and
  83. self.backend_domain == other.backend_domain and
  84. self.ident == other.ident
  85. )
  86. except AttributeError:
  87. return False
  88. def __str__(self):
  89. return '{!s}:{!s}'.format(self.backend_domain, self.ident)
  90. class UnknownDevice(DeviceInfo):
  91. # pylint: disable=too-few-public-methods
  92. """Unknown device - for example exposed by domain not running currently"""
  93. def __init__(self, backend_domain, devclass, ident, description=None,
  94. **kwargs):
  95. if description is None:
  96. description = "Unknown device"
  97. super().__init__(backend_domain, devclass, ident,
  98. description, **kwargs)
  99. class DeviceCollection(object):
  100. """Bag for devices.
  101. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  102. :param vm: VM for which we manage devices
  103. :param class_: device class
  104. """
  105. def __init__(self, vm, class_):
  106. self._vm = vm
  107. self._class = class_
  108. self._dev_cache = {}
  109. def attach(self, device_assignment):
  110. """Attach (add) device to domain.
  111. :param DeviceAssignment device_assignment: device object
  112. """
  113. if not device_assignment.frontend_domain:
  114. device_assignment.frontend_domain = self._vm
  115. else:
  116. assert device_assignment.frontend_domain == self._vm, \
  117. "Trying to attach DeviceAssignment belonging to other domain"
  118. if device_assignment.devclass is None:
  119. device_assignment.devclass = self._class
  120. else:
  121. assert device_assignment.devclass == self._class
  122. options = device_assignment.options.copy()
  123. if device_assignment.persistent:
  124. options['persistent'] = 'True'
  125. options_str = ' '.join('{}={}'.format(opt, val)
  126. for opt, val in sorted(options.items()))
  127. self._vm.qubesd_call(None,
  128. 'admin.vm.device.{}.Attach'.format(self._class),
  129. '{!s}+{!s}'.format(
  130. device_assignment.backend_domain,
  131. device_assignment.ident),
  132. options_str.encode('utf-8'))
  133. def detach(self, device_assignment):
  134. """Detach (remove) device from domain.
  135. :param DeviceAssignment device_assignment: device to detach
  136. (obtained from :py:meth:`assignments`)
  137. """
  138. if not device_assignment.frontend_domain:
  139. device_assignment.frontend_domain = self._vm
  140. else:
  141. assert device_assignment.frontend_domain == self._vm, \
  142. "Trying to detach DeviceAssignment belonging to other domain"
  143. if device_assignment.devclass is None:
  144. device_assignment.devclass = self._class
  145. else:
  146. assert device_assignment.devclass == self._class
  147. self._vm.qubesd_call(None,
  148. 'admin.vm.device.{}.Detach'.format(self._class),
  149. '{!s}+{!s}'.format(
  150. device_assignment.backend_domain,
  151. device_assignment.ident))
  152. def assignments(self, persistent=None):
  153. """List assignments for devices which are (or may be) attached to the
  154. vm.
  155. Devices may be attached persistently (so they are included in
  156. :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
  157. but be temporarily detached.
  158. :param bool persistent: only include devices which are or are not
  159. attached persistently.
  160. """
  161. assignments_str = self._vm.qubesd_call(None,
  162. 'admin.vm.device.{}.List'.format(
  163. self._class)).decode()
  164. for assignment_str in assignments_str.splitlines():
  165. device, _, options_all = assignment_str.partition(' ')
  166. backend_domain, ident = device.split('+', 1)
  167. options = dict(opt_single.split('=', 1)
  168. for opt_single in options_all.split(' ') if
  169. opt_single)
  170. dev_persistent = (options.pop('persistent', False) in
  171. ['True', 'yes', True])
  172. if persistent is not None and dev_persistent != persistent:
  173. continue
  174. backend_domain = self._vm.app.domains.get_blind(backend_domain)
  175. yield DeviceAssignment(backend_domain, ident, options,
  176. persistent=dev_persistent,
  177. frontend_domain=self._vm,
  178. devclass=self._class)
  179. def attached(self):
  180. """List devices which are (or may be) attached to this vm """
  181. for assignment in self.assignments():
  182. yield assignment.device
  183. def persistent(self):
  184. """ Devices persistently attached and safe to access before libvirt
  185. bootstrap.
  186. """
  187. for assignment in self.assignments(True):
  188. yield assignment.device
  189. def available(self):
  190. """List devices exposed by this vm"""
  191. devices_str = \
  192. self._vm.qubesd_call(None,
  193. 'admin.vm.device.{}.Available'.format(
  194. self._class)).decode()
  195. for dev_str in devices_str.splitlines():
  196. ident, _, info = dev_str.partition(' ')
  197. # description is special that it can contain spaces
  198. info, _, description = info.partition('description=')
  199. info_dict = dict(info_single.split('=', 1)
  200. for info_single in info.split(' ') if info_single)
  201. yield DeviceInfo(self._vm, self._class, ident,
  202. description=description,
  203. **info_dict)
  204. def update_persistent(self, device, persistent):
  205. """Update `persistent` flag of already attached device.
  206. :param DeviceInfo device: device for which change persistent flag
  207. :param bool persistent: new persistent flag
  208. """
  209. self._vm.qubesd_call(None,
  210. 'admin.vm.device.{}.Set.persistent'.format(
  211. self._class),
  212. '{!s}+{!s}'.format(device.backend_domain,
  213. device.ident),
  214. str(persistent).encode('utf-8'))
  215. __iter__ = available
  216. def clear_cache(self):
  217. """Clear cache of available devices"""
  218. self._dev_cache.clear()
  219. def __getitem__(self, item):
  220. """Get device object with given ident.
  221. :returns: py:class:`DeviceInfo`
  222. If domain isn't running, it is impossible to check device validity,
  223. so return UnknownDevice object. Also do the same for non-existing
  224. devices - otherwise it will be impossible to detach already
  225. disconnected device.
  226. """
  227. # fist, check if we have cached device info
  228. if item in self._dev_cache:
  229. return self._dev_cache[item]
  230. # then look for available devices
  231. for dev in self.available():
  232. if dev.ident == item:
  233. self._dev_cache[item] = dev
  234. return dev
  235. # if still nothing, return UnknownDevice instance for the reason
  236. # explained in docstring, but don't cache it
  237. return UnknownDevice(self._vm, self._class, item)
  238. class DeviceManager(dict):
  239. """Device manager that hold all devices by their classes.
  240. :param vm: VM for which we manage devices
  241. """
  242. def __init__(self, vm):
  243. super().__init__()
  244. self._vm = vm
  245. def __missing__(self, key):
  246. self[key] = DeviceCollection(self._vm, key)
  247. return self[key]
  248. def __iter__(self):
  249. return iter(self._vm.app.list_deviceclass())
  250. def keys(self):
  251. return self._vm.app.list_deviceclass()