devices.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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 DeviceCollection(object):
  47. '''Bag for devices.
  48. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  49. :param vm: VM for which we manage devices
  50. :param class_: device class
  51. This class emits following events on VM object:
  52. .. event:: device-attach:<class> (device)
  53. Fired when device is attached to a VM.
  54. :param device: :py:class:`DeviceInfo` object to be attached
  55. .. event:: device-pre-attach:<class> (device)
  56. Fired before device is attached to a VM
  57. :param device: :py:class:`DeviceInfo` object to be attached
  58. .. event:: device-detach:<class> (device)
  59. Fired when device is detached from a VM.
  60. :param device: :py:class:`DeviceInfo` object to be attached
  61. .. event:: device-pre-detach:<class> (device)
  62. Fired before device is detached from a VM
  63. :param device: :py:class:`DeviceInfo` object to be attached
  64. .. event:: device-list:<class>
  65. Fired to get list of devices exposed by a VM. Handlers of this
  66. event should return a list of py:class:`DeviceInfo` objects (or
  67. appropriate class specific descendant)
  68. .. event:: device-get:<class> (ident)
  69. Fired to get a single device, given by the `ident` parameter.
  70. Handlers of this event should either return appropriate object of
  71. :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
  72. raise :py:class:`exceptions.KeyError`.
  73. .. event:: device-list-attached:<class> (persistent)
  74. Fired to get list of currently attached devices to a VM. Handlers
  75. of this event should return list of devices actually attached to
  76. a domain, regardless of its settings.
  77. '''
  78. def __init__(self, vm, class_):
  79. self._vm = vm
  80. self._class = class_
  81. self._set = set()
  82. self.devclass = qubes.utils.get_entry_point_one(
  83. 'qubes.devices', self._class)
  84. def attach(self, device):
  85. '''Attach (add) device to domain.
  86. :param DeviceInfo device: device object
  87. '''
  88. if device in self.attached():
  89. raise KeyError(
  90. 'device {!r} of class {} already attached to {!r}'.format(
  91. device, self._class, self._vm))
  92. self._vm.fire_event_pre('device-pre-attach:' + self._class, device)
  93. self._set.add(device)
  94. self._vm.fire_event('device-attach:' + self._class, device)
  95. def detach(self, device):
  96. '''Detach (remove) device from domain.
  97. :param DeviceInfo device: device object
  98. '''
  99. if device not in self.attached():
  100. raise KeyError(
  101. 'device {!r} of class {} not attached to {!r}'.format(
  102. device, self._class, self._vm))
  103. self._vm.fire_event_pre('device-pre-detach:' + self._class, device)
  104. self._set.remove(device)
  105. self._vm.fire_event('device-detach:' + self._class, device)
  106. def attached(self, persistent=None):
  107. '''List devices which are (or may be) attached to this vm
  108. Devices may be attached persistently (so they are included in
  109. :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
  110. but be temporarily detached.
  111. :param bool persistent: only include devices which are (or are not) \
  112. attached persistently - None means both
  113. '''
  114. seen = self._set.copy()
  115. # ask for really attached devices only when requested not only
  116. # persistent ones
  117. if persistent is not True:
  118. attached = self._vm.fire_event(
  119. 'device-list-attached:' + self._class,
  120. persistent=persistent)
  121. for device in attached:
  122. device_persistent = device in self._set
  123. if persistent is not None and device_persistent != persistent:
  124. continue
  125. assert device.frontend_domain == self._vm, \
  126. '{!r} != {!r}'.format(device.frontend_domain, self._vm)
  127. yield device
  128. try:
  129. seen.remove(device)
  130. except KeyError:
  131. pass
  132. if persistent is False:
  133. return
  134. for device in seen:
  135. # get fresh object - may contain updated information
  136. device = device.backend_domain.devices[self._class][device.ident]
  137. yield device
  138. def available(self):
  139. '''List devices exposed by this vm'''
  140. devices = self._vm.fire_event('device-list:' + self._class)
  141. return devices
  142. def __iter__(self):
  143. return iter(self.available())
  144. def __getitem__(self, ident):
  145. '''Get device object with given ident.
  146. :returns: py:class:`DeviceInfo`
  147. If domain isn't running, it is impossible to check device validity,
  148. so return UnknownDevice object. Also do the same for non-existing
  149. devices - otherwise it will be impossible to detach already
  150. disconnected device.
  151. :raises AssertionError: when multiple devices with the same ident are
  152. found
  153. '''
  154. dev = self._vm.fire_event('device-get:' + self._class, ident)
  155. if dev:
  156. assert len(dev) == 1
  157. return dev[0]
  158. else:
  159. return UnknownDevice(self._vm, ident)
  160. class DeviceManager(dict):
  161. '''Device manager that hold all devices by their classess.
  162. :param vm: VM for which we manage devices
  163. '''
  164. def __init__(self, vm):
  165. super(DeviceManager, self).__init__()
  166. self._vm = vm
  167. def __missing__(self, key):
  168. self[key] = DeviceCollection(self._vm, key)
  169. return self[key]
  170. class DeviceInfo(object):
  171. # pylint: disable=too-few-public-methods
  172. def __init__(self, backend_domain, ident, description=None,
  173. frontend_domain=None, **kwargs):
  174. #: domain providing this device
  175. self.backend_domain = backend_domain
  176. #: device identifier (unique for given domain and device type)
  177. self.ident = ident
  178. #: human readable description/name of the device
  179. self.description = description
  180. #: (running) domain to which device is currently attached
  181. self.frontend_domain = frontend_domain
  182. self.data = kwargs
  183. if hasattr(self, 'regex'):
  184. # pylint: disable=no-member
  185. dev_match = self.regex.match(ident)
  186. if not dev_match:
  187. raise ValueError('Invalid device identifier: {!r}'.format(
  188. ident))
  189. for group in self.regex.groupindex:
  190. setattr(self, group, dev_match.group(group))
  191. def __hash__(self):
  192. return hash(self.ident)
  193. def __eq__(self, other):
  194. return (
  195. self.backend_domain == other.backend_domain and
  196. self.ident == other.ident
  197. )
  198. class UnknownDevice(DeviceInfo):
  199. # pylint: disable=too-few-public-methods
  200. '''Unknown device - for example exposed by domain not running currently'''
  201. def __init__(self, backend_domain, ident, description=None,
  202. frontend_domain=None, **kwargs):
  203. if description is None:
  204. description = "Unknown device"
  205. super(UnknownDevice, self).__init__(backend_domain, ident, description,
  206. frontend_domain, **kwargs)
  207. class BlockDevice(object):
  208. # pylint: disable=too-few-public-methods
  209. def __init__(self, path, name, script=None, rw=True, domain=None,
  210. devtype='disk'):
  211. assert name, 'Missing device name'
  212. assert path, 'Missing device path'
  213. self.path = path
  214. self.name = name
  215. self.rw = rw
  216. self.script = script
  217. self.domain = domain
  218. self.devtype = devtype