devices.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2010-2016 Joanna Rutkowska <joanna@invisiblethingslab.com>
  5. # Copyright (C) 2015-2016 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. #
  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. Such extension should provide:
  30. - `qubes.devices` endpoint - a class descendant from
  31. :py:class:`qubes.devices.DeviceInfo`, designed to hold device description (
  32. including class-specific properties)
  33. - handle `device-attach:class` and `device-detach:class` events for
  34. performing the attach/detach action; events are fired even when domain isn't
  35. running and extension should be prepared for this
  36. - handle `device-list:class` event - list devices exposed by particular
  37. domain; it should return list of appropriate DeviceInfo objects
  38. - handle `device-get:class` event - get one device object exposed by this
  39. domain of given identifier
  40. - handle `device-list-attached:class` event - list currently attached
  41. devices to this domain
  42. '''
  43. import qubes.utils
  44. class DeviceNotAttached(qubes.exc.QubesException, KeyError):
  45. '''Trying to detach not attached device'''
  46. pass
  47. class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError):
  48. '''Trying to attach already attached device'''
  49. pass
  50. class DeviceCollection(object):
  51. '''Bag for devices.
  52. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  53. :param vm: VM for which we manage devices
  54. :param class_: device class
  55. This class emits following events on VM object:
  56. .. event:: device-attach:<class> (device)
  57. Fired when device is attached to a VM.
  58. :param device: :py:class:`DeviceInfo` object to be attached
  59. .. event:: device-pre-attach:<class> (device)
  60. Fired before device is attached to a VM
  61. :param device: :py:class:`DeviceInfo` object to be attached
  62. .. event:: device-detach:<class> (device)
  63. Fired when device is detached from a VM.
  64. :param device: :py:class:`DeviceInfo` object to be attached
  65. .. event:: device-pre-detach:<class> (device)
  66. Fired before device is detached from a VM
  67. :param device: :py:class:`DeviceInfo` object to be attached
  68. .. event:: device-list:<class>
  69. Fired to get list of devices exposed by a VM. Handlers of this
  70. event should return a list of py:class:`DeviceInfo` objects (or
  71. appropriate class specific descendant)
  72. .. event:: device-get:<class> (ident)
  73. Fired to get a single device, given by the `ident` parameter.
  74. Handlers of this event should either return appropriate object of
  75. :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
  76. raise :py:class:`exceptions.KeyError`.
  77. .. event:: device-list-attached:<class> (persistent)
  78. Fired to get list of currently attached devices to a VM. Handlers
  79. of this event should return list of devices actually attached to
  80. a domain, regardless of its settings.
  81. '''
  82. def __init__(self, vm, class_):
  83. self._vm = vm
  84. self._class = class_
  85. self._set = set()
  86. self.devclass = qubes.utils.get_entry_point_one(
  87. 'qubes.devices', self._class)
  88. def attach(self, device, persistent=True):
  89. '''Attach (add) device to domain.
  90. :param DeviceInfo device: device object
  91. '''
  92. if device in self.attached():
  93. raise DeviceAlreadyAttached(
  94. 'device {!r} of class {} already attached to {!r}'.format(
  95. device, self._class, self._vm))
  96. self._vm.fire_event_pre('device-pre-attach:'+self._class, device=device)
  97. if persistent:
  98. self._set.add(device)
  99. self._vm.fire_event('device-attach:' + self._class, device=device)
  100. def detach(self, device, persistent=True):
  101. '''Detach (remove) device from domain.
  102. :param DeviceInfo device: device object
  103. '''
  104. if device not in self.attached():
  105. raise DeviceNotAttached(
  106. 'device {!s} of class {} not attached to {!s}'.format(
  107. device, self._class, self._vm))
  108. self._vm.fire_event_pre('device-pre-detach:'+self._class, device=device)
  109. if persistent:
  110. self._set.remove(device)
  111. self._vm.fire_event('device-detach:' + self._class, device=device)
  112. def attached(self, persistent=None):
  113. '''List devices which are (or may be) attached to this vm
  114. Devices may be attached persistently (so they are included in
  115. :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
  116. but be temporarily detached.
  117. :param bool persistent: only include devices which are (or are not) \
  118. attached persistently - None means both
  119. '''
  120. seen = self._set.copy()
  121. # ask for really attached devices only when requested not only
  122. # persistent ones
  123. if persistent is not True:
  124. attached = self._vm.fire_event(
  125. 'device-list-attached:' + self._class,
  126. persistent=persistent)
  127. for device in attached:
  128. device_persistent = device in self._set
  129. if persistent is not None and device_persistent != persistent:
  130. continue
  131. assert device.frontend_domain == self._vm, \
  132. '{!r} != {!r}'.format(device.frontend_domain, self._vm)
  133. yield device
  134. try:
  135. seen.remove(device)
  136. except KeyError:
  137. pass
  138. if persistent is False:
  139. return
  140. for device in seen:
  141. # get fresh object - may contain updated information
  142. device = device.backend_domain.devices[self._class][device.ident]
  143. yield device
  144. def available(self):
  145. '''List devices exposed by this vm'''
  146. devices = self._vm.fire_event('device-list:' + self._class)
  147. return devices
  148. def __iter__(self):
  149. return iter(self.available())
  150. def __getitem__(self, ident):
  151. '''Get device object with given ident.
  152. :returns: py:class:`DeviceInfo`
  153. If domain isn't running, it is impossible to check device validity,
  154. so return UnknownDevice object. Also do the same for non-existing
  155. devices - otherwise it will be impossible to detach already
  156. disconnected device.
  157. :raises AssertionError: when multiple devices with the same ident are
  158. found
  159. '''
  160. dev = self._vm.fire_event('device-get:' + self._class, ident=ident)
  161. if dev:
  162. assert len(dev) == 1
  163. return dev[0]
  164. else:
  165. return UnknownDevice(self._vm, ident)
  166. class DeviceManager(dict):
  167. '''Device manager that hold all devices by their classess.
  168. :param vm: VM for which we manage devices
  169. '''
  170. def __init__(self, vm):
  171. super(DeviceManager, self).__init__()
  172. self._vm = vm
  173. def __missing__(self, key):
  174. self[key] = DeviceCollection(self._vm, key)
  175. return self[key]
  176. class DeviceInfo(object):
  177. # pylint: disable=too-few-public-methods
  178. def __init__(self, backend_domain, ident, description=None,
  179. frontend_domain=None, **kwargs):
  180. #: domain providing this device
  181. self.backend_domain = backend_domain
  182. #: device identifier (unique for given domain and device type)
  183. self.ident = ident
  184. # allow redefining those as dynamic properties in subclasses
  185. try:
  186. #: human readable description/name of the device
  187. self.description = description
  188. except AttributeError:
  189. pass
  190. try:
  191. #: (running) domain to which device is currently attached
  192. self.frontend_domain = frontend_domain
  193. except AttributeError:
  194. pass
  195. self.data = kwargs
  196. if hasattr(self, 'regex'):
  197. # pylint: disable=no-member
  198. dev_match = self.regex.match(ident)
  199. if not dev_match:
  200. raise ValueError('Invalid device identifier: {!r}'.format(
  201. ident))
  202. for group in self.regex.groupindex:
  203. setattr(self, group, dev_match.group(group))
  204. def __hash__(self):
  205. return hash(self.ident)
  206. def __eq__(self, other):
  207. return (
  208. self.backend_domain == other.backend_domain and
  209. self.ident == other.ident
  210. )
  211. def __str__(self):
  212. return '{!s}:{!s}'.format(self.backend_domain, self.ident)
  213. class UnknownDevice(DeviceInfo):
  214. # pylint: disable=too-few-public-methods
  215. '''Unknown device - for example exposed by domain not running currently'''
  216. def __init__(self, backend_domain, ident, description=None,
  217. frontend_domain=None, **kwargs):
  218. if description is None:
  219. description = "Unknown device"
  220. super(UnknownDevice, self).__init__(backend_domain, ident, description,
  221. frontend_domain, **kwargs)
  222. class BlockDevice(object):
  223. # pylint: disable=too-few-public-methods
  224. def __init__(self, path, name, script=None, rw=True, domain=None,
  225. devtype='disk'):
  226. assert name, 'Missing device name'
  227. assert path, 'Missing device path'
  228. self.path = path
  229. self.name = name
  230. self.rw = rw
  231. self.script = script
  232. self.domain = domain
  233. self.devtype = devtype