devices.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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` and can contain only characters from `[a-zA-Z0-9._-]` set.
  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 DeviceAssignment(object): # pylint: disable=too-few-public-methods
  51. ''' Maps a device to a frontend_domain. '''
  52. def __init__(self, backend_domain, ident, options=None, persistent=False,
  53. frontend_domain=None):
  54. self.backend_domain = backend_domain
  55. self.ident = ident
  56. self.options = options or {}
  57. self.persistent = persistent
  58. self.frontend_domain = frontend_domain
  59. def __repr__(self):
  60. return "[%s]:%s" % (self.backend_domain, self.ident)
  61. def __hash__(self):
  62. return hash((self.backend_domain, self.ident))
  63. def __eq__(self, other):
  64. if not isinstance(self, other.__class__):
  65. return NotImplemented
  66. return self.backend_domain == other.backend_domain \
  67. and self.ident == other.ident
  68. class DeviceCollection(object):
  69. '''Bag for devices.
  70. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  71. :param vm: VM for which we manage devices
  72. :param class_: device class
  73. This class emits following events on VM object:
  74. .. event:: device-attach:<class> (device)
  75. Fired when device is attached to a VM.
  76. :param device: :py:class:`DeviceInfo` object to be attached
  77. .. event:: device-pre-attach:<class> (device)
  78. Fired before device is attached to a VM
  79. :param device: :py:class:`DeviceInfo` object to be attached
  80. .. event:: device-detach:<class> (device)
  81. Fired when device is detached from a VM.
  82. :param device: :py:class:`DeviceInfo` object to be attached
  83. .. event:: device-pre-detach:<class> (device)
  84. Fired before device is detached from a VM
  85. :param device: :py:class:`DeviceInfo` object to be attached
  86. .. event:: device-list:<class>
  87. Fired to get list of devices exposed by a VM. Handlers of this
  88. event should return a list of py:class:`DeviceInfo` objects (or
  89. appropriate class specific descendant)
  90. .. event:: device-get:<class> (ident)
  91. Fired to get a single device, given by the `ident` parameter.
  92. Handlers of this event should either return appropriate object of
  93. :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
  94. raise :py:class:`exceptions.KeyError`.
  95. .. event:: device-list-attached:<class> (persistent)
  96. Fired to get list of currently attached devices to a VM. Handlers
  97. of this event should return list of devices actually attached to
  98. a domain, regardless of its settings.
  99. '''
  100. def __init__(self, vm, class_):
  101. self._vm = vm
  102. self._class = class_
  103. self._set = PersistentCollection()
  104. self.devclass = qubes.utils.get_entry_point_one(
  105. 'qubes.devices', self._class)
  106. def attach(self, device_assignment: DeviceAssignment):
  107. '''Attach (add) device to domain.
  108. :param DeviceInfo device: device object
  109. '''
  110. if not device_assignment.frontend_domain:
  111. device_assignment.frontend_domain = self._vm
  112. else:
  113. assert device_assignment.frontend_domain == self._vm, \
  114. "Trying to attach DeviceAssignment belonging to other domain"
  115. if not device_assignment.persistent and self._vm.is_halted():
  116. raise qubes.exc.QubesVMNotRunningError(self._vm,
  117. "Devices can only be attached non-persistent to a running vm")
  118. device = self._device(device_assignment)
  119. if device in self.assignments():
  120. raise DeviceAlreadyAttached(
  121. 'device {!s} of class {} already attached to {!s}'.format(
  122. device, self._class, self._vm))
  123. self._vm.fire_event_pre('device-pre-attach:'+self._class, device=device)
  124. if device_assignment.persistent:
  125. self._set.add(device_assignment)
  126. self._vm.fire_event('device-attach:' + self._class, device=device)
  127. def detach(self, device_assignment: DeviceAssignment):
  128. '''Detach (remove) device from domain.
  129. :param DeviceInfo device: device object
  130. '''
  131. if not device_assignment.frontend_domain:
  132. device_assignment.frontend_domain = self._vm
  133. if device_assignment in self._set and not self._vm.is_halted():
  134. raise qubes.exc.QubesVMNotHaltedError(self._vm,
  135. "Can not remove a persistent attachment from a non halted vm")
  136. if device_assignment not in self.assignments():
  137. raise DeviceNotAttached(
  138. 'device {!s} of class {} not attached to {!s}'.format(
  139. device_assignment.ident, self._class, self._vm))
  140. device = self._device(device_assignment)
  141. self._vm.fire_event_pre('device-pre-detach:'+self._class, device=device)
  142. if device in self._set:
  143. device_assignment.persistent = True
  144. self._set.discard(device_assignment)
  145. self._vm.fire_event('device-detach:' + self._class, device=device)
  146. def attached(self):
  147. '''List devices which are (or may be) attached to this vm '''
  148. attached = self._vm.fire_event('device-list-attached:' + self._class)
  149. if attached:
  150. return [dev for dev, _ in attached]
  151. return []
  152. def persistent(self):
  153. ''' Devices persistently attached and safe to access before libvirt
  154. bootstrap.
  155. '''
  156. return [self._device(a) for a in self._set]
  157. def assignments(self, persistent=None):
  158. '''List assignments for devices which are (or may be) attached to the
  159. vm.
  160. Devices may be attached persistently (so they are included in
  161. :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
  162. but be temporarily detached.
  163. :param bool persistent: only include devices which are or are not
  164. attached persistently.
  165. '''
  166. devices = self._vm.fire_event('device-list-attached:' + self._class,
  167. persistent=persistent)
  168. result = []
  169. for dev, options in devices:
  170. if dev in self._set and not persistent:
  171. continue
  172. elif dev in self._set:
  173. result.append(self._set.get(dev))
  174. elif dev not in self._set and persistent:
  175. continue
  176. else:
  177. result.append(
  178. DeviceAssignment(backend_domain=dev.backend_domain,
  179. ident=dev.ident, options=options,
  180. frontend_domain=self._vm))
  181. if persistent is not False and not result:
  182. result.extend(self._set)
  183. return result
  184. def available(self):
  185. '''List devices exposed by this vm'''
  186. devices = self._vm.fire_event('device-list:' + self._class)
  187. return devices
  188. def _device(self, assignment: DeviceAssignment):
  189. ''' Helper method for geting a `qubes.devices.DeviceInfo` object from
  190. `qubes.devices.DeviceAssignment`. '''
  191. return assignment.backend_domain.devices[self._class][assignment.ident]
  192. def __iter__(self):
  193. return iter(self.available())
  194. def __getitem__(self, ident):
  195. '''Get device object with given ident.
  196. :returns: py:class:`DeviceInfo`
  197. If domain isn't running, it is impossible to check device validity,
  198. so return UnknownDevice object. Also do the same for non-existing
  199. devices - otherwise it will be impossible to detach already
  200. disconnected device.
  201. :raises AssertionError: when multiple devices with the same ident are
  202. found
  203. '''
  204. dev = self._vm.fire_event('device-get:' + self._class, ident=ident)
  205. if dev:
  206. assert len(dev) == 1
  207. return dev[0]
  208. return UnknownDevice(self._vm, ident)
  209. class DeviceManager(dict):
  210. '''Device manager that hold all devices by their classess.
  211. :param vm: VM for which we manage devices
  212. '''
  213. def __init__(self, vm):
  214. super(DeviceManager, self).__init__()
  215. self._vm = vm
  216. def __missing__(self, key):
  217. self[key] = DeviceCollection(self._vm, key)
  218. return self[key]
  219. class DeviceInfo(object):
  220. ''' Holds all information about a device '''
  221. # pylint: disable=too-few-public-methods
  222. def __init__(self, backend_domain, ident, description=None,
  223. frontend_domain=None, options=None, **kwargs):
  224. #: domain providing this device
  225. self.backend_domain = backend_domain
  226. #: device identifier (unique for given domain and device type)
  227. self.ident = ident
  228. # allow redefining those as dynamic properties in subclasses
  229. try:
  230. #: human readable description/name of the device
  231. self.description = description
  232. except AttributeError:
  233. pass
  234. try:
  235. #: (running) domain to which device is currently attached
  236. self.frontend_domain = frontend_domain
  237. except AttributeError:
  238. pass
  239. self.options = options or dict()
  240. self.data = kwargs
  241. if hasattr(self, 'regex'):
  242. # pylint: disable=no-member
  243. dev_match = self.regex.match(ident)
  244. if not dev_match:
  245. raise ValueError('Invalid device identifier: {!r}'.format(
  246. ident))
  247. for group in self.regex.groupindex:
  248. setattr(self, group, dev_match.group(group))
  249. def __hash__(self):
  250. return hash(self.ident)
  251. def __eq__(self, other):
  252. return (
  253. self.backend_domain == other.backend_domain and
  254. self.ident == other.ident
  255. )
  256. def __str__(self):
  257. return '{!s}:{!s}'.format(self.backend_domain, self.ident)
  258. class UnknownDevice(DeviceInfo):
  259. # pylint: disable=too-few-public-methods
  260. '''Unknown device - for example exposed by domain not running currently'''
  261. def __init__(self, backend_domain, ident, description=None,
  262. frontend_domain=None, **kwargs):
  263. if description is None:
  264. description = "Unknown device"
  265. super(UnknownDevice, self).__init__(backend_domain, ident, description,
  266. frontend_domain, **kwargs)
  267. class PersistentCollection(object):
  268. ''' Helper object managing persistent `DeviceAssignment`s.
  269. '''
  270. def __init__(self):
  271. self._dict = {}
  272. def add(self, assignment: DeviceAssignment):
  273. ''' Add assignment to collection '''
  274. assert assignment.persistent and assignment.frontend_domain
  275. vm = assignment.backend_domain
  276. ident = assignment.ident
  277. key = (vm, ident)
  278. assert key not in self._dict
  279. self._dict[key] = assignment
  280. def discard(self, assignment):
  281. ''' Discard assignment from collection '''
  282. assert assignment.persistent and assignment.frontend_domain
  283. vm = assignment.backend_domain
  284. ident = assignment.ident
  285. key = (vm, ident)
  286. if key not in self._dict:
  287. raise KeyError
  288. del self._dict[key]
  289. def __contains__(self, device) -> bool:
  290. return (device.backend_domain, device.ident) in self._dict
  291. def get(self, device: DeviceInfo) -> DeviceAssignment:
  292. ''' Returns the corresponding `qubes.devices.DeviceAssignment` for the
  293. device. '''
  294. return self._dict[(device.backend_domain, device.ident)]
  295. def __iter__(self):
  296. return self._dict.values().__iter__()
  297. def __len__(self) -> int:
  298. return len(self._dict.keys())