devices.py 10 KB

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