devices.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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 library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library 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 GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. #
  21. '''API for various types of devices.
  22. Main concept is that some domain main
  23. expose (potentially multiple) devices, which can be attached to other domains.
  24. Devices can be of different buses (like 'pci', 'usb', etc). Each device
  25. bus is implemented by an extension.
  26. Devices are identified by pair of (backend domain, `ident`), where `ident` is
  27. :py:class:`str` and can contain only characters from `[a-zA-Z0-9._-]` set.
  28. Such extension should provide:
  29. - `qubes.devices` endpoint - a class descendant from
  30. :py:class:`qubes.devices.DeviceInfo`, designed to hold device description (
  31. including bus-specific properties)
  32. - handle `device-attach:bus` and `device-detach:bus` events for
  33. performing the attach/detach action; events are fired even when domain isn't
  34. running and extension should be prepared for this; handlers for those events
  35. can be coroutines
  36. - handle `device-list:bus` event - list devices exposed by particular
  37. domain; it should return list of appropriate DeviceInfo objects
  38. - handle `device-get:bus` 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. - fire `device-list-change:class` event when device list change is detected
  43. (new/removed device)
  44. Note that device-listing event handlers can not be asynchronous. This for
  45. example means you can not call qrexec service there. This is intentional to
  46. keep device listing operation cheap. You need to design the extension to take
  47. this into account (for example by using QubesDB).
  48. Extension may use QubesDB watch API (QubesVM.watch_qdb_path(path), then handle
  49. `domain-qdb-change:path`) to detect changes and fire
  50. `device-list-change:class` event.
  51. '''
  52. import asyncio
  53. import qubes.utils
  54. class DeviceNotAttached(qubes.exc.QubesException, KeyError):
  55. '''Trying to detach not attached device'''
  56. pass
  57. class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError):
  58. '''Trying to attach already attached device'''
  59. pass
  60. class DeviceInfo:
  61. ''' Holds all information about a device '''
  62. # pylint: disable=too-few-public-methods
  63. def __init__(self, backend_domain, ident, description=None,
  64. frontend_domain=None):
  65. #: domain providing this device
  66. self.backend_domain = backend_domain
  67. #: device identifier (unique for given domain and device type)
  68. self.ident = ident
  69. # allow redefining those as dynamic properties in subclasses
  70. try:
  71. #: human readable description/name of the device
  72. self.description = description
  73. except AttributeError:
  74. pass
  75. try:
  76. #: (running) domain to which device is currently attached
  77. self.frontend_domain = frontend_domain
  78. except AttributeError:
  79. pass
  80. if hasattr(self, 'regex'):
  81. # pylint: disable=no-member
  82. dev_match = self.regex.match(ident)
  83. if not dev_match:
  84. raise ValueError('Invalid device identifier: {!r}'.format(
  85. ident))
  86. for group in self.regex.groupindex:
  87. setattr(self, group, dev_match.group(group))
  88. def __hash__(self):
  89. return hash((self.backend_domain, self.ident))
  90. def __eq__(self, other):
  91. return (
  92. self.backend_domain == other.backend_domain and
  93. self.ident == other.ident
  94. )
  95. def __lt__(self, other):
  96. if isinstance(other, DeviceInfo):
  97. return (self.backend_domain, self.ident) < \
  98. (other.backend_domain, other.ident)
  99. return NotImplemented
  100. def __str__(self):
  101. return '{!s}:{!s}'.format(self.backend_domain, self.ident)
  102. class DeviceAssignment: # pylint: disable=too-few-public-methods
  103. ''' Maps a device to a frontend_domain. '''
  104. def __init__(self, backend_domain, ident, options=None, persistent=False,
  105. bus=None):
  106. self.backend_domain = backend_domain
  107. self.ident = ident
  108. self.options = options or {}
  109. self.persistent = persistent
  110. self.bus = bus
  111. def __repr__(self):
  112. return "[%s]:%s" % (self.backend_domain, self.ident)
  113. def __hash__(self):
  114. # it's important to use the same hash as DeviceInfo
  115. return hash((self.backend_domain, self.ident))
  116. def __eq__(self, other):
  117. if not isinstance(self, other.__class__):
  118. return NotImplemented
  119. return self.backend_domain == other.backend_domain \
  120. and self.ident == other.ident
  121. def clone(self):
  122. '''Clone object instance'''
  123. return self.__class__(
  124. self.backend_domain,
  125. self.ident,
  126. self.options,
  127. self.persistent,
  128. self.bus,
  129. )
  130. @property
  131. def device(self):
  132. '''Get DeviceInfo object corresponding to this DeviceAssignment'''
  133. return self.backend_domain.devices[self.bus][self.ident]
  134. class DeviceCollection:
  135. '''Bag for devices.
  136. Used as default value for :py:meth:`DeviceManager.__missing__` factory.
  137. :param vm: VM for which we manage devices
  138. :param bus: device bus
  139. This class emits following events on VM object:
  140. .. event:: device-attach:<class> (device, options)
  141. Fired when device is attached to a VM.
  142. Handler for this event can be asynchronous (a coroutine).
  143. :param device: :py:class:`DeviceInfo` object to be attached
  144. :param options: :py:class:`dict` of attachment options
  145. .. event:: device-pre-attach:<class> (device)
  146. Fired before device is attached to a VM
  147. Handler for this event can be asynchronous (a coroutine).
  148. :param device: :py:class:`DeviceInfo` object to be attached
  149. .. event:: device-detach:<class> (device)
  150. Fired when device is detached from a VM.
  151. Handler for this event can be asynchronous (a coroutine).
  152. :param device: :py:class:`DeviceInfo` object to be attached
  153. .. event:: device-pre-detach:<class> (device)
  154. Fired before device is detached from a VM
  155. Handler for this event can be asynchronous (a coroutine).
  156. :param device: :py:class:`DeviceInfo` object to be attached
  157. .. event:: device-list:<class>
  158. Fired to get list of devices exposed by a VM. Handlers of this
  159. event should return a list of py:class:`DeviceInfo` objects (or
  160. appropriate class specific descendant)
  161. .. event:: device-get:<class> (ident)
  162. Fired to get a single device, given by the `ident` parameter.
  163. Handlers of this event should either return appropriate object of
  164. :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not
  165. raise :py:class:`exceptions.KeyError`.
  166. .. event:: device-list-attached:<class> (persistent)
  167. Fired to get list of currently attached devices to a VM. Handlers
  168. of this event should return list of devices actually attached to
  169. a domain, regardless of its settings.
  170. '''
  171. def __init__(self, vm, bus):
  172. self._vm = vm
  173. self._bus = bus
  174. self._set = PersistentCollection()
  175. self.devclass = qubes.utils.get_entry_point_one(
  176. 'qubes.devices', self._bus)
  177. @asyncio.coroutine
  178. def attach(self, device_assignment: DeviceAssignment):
  179. '''Attach (add) device to domain.
  180. :param DeviceInfo device: device object
  181. '''
  182. if device_assignment.bus is None:
  183. device_assignment.bus = self._bus
  184. else:
  185. assert device_assignment.bus == self._bus, \
  186. "Trying to attach DeviceAssignment of a different device class"
  187. if not device_assignment.persistent and self._vm.is_halted():
  188. raise qubes.exc.QubesVMNotRunningError(self._vm,
  189. "VM not running, can only attach device with persistent flag")
  190. device = device_assignment.device
  191. if device in self.assignments():
  192. raise DeviceAlreadyAttached(
  193. 'device {!s} of class {} already attached to {!s}'.format(
  194. device, self._bus, self._vm))
  195. yield from self._vm.fire_event_async('device-pre-attach:' + self._bus,
  196. pre_event=True,
  197. device=device, options=device_assignment.options)
  198. if device_assignment.persistent:
  199. self._set.add(device_assignment)
  200. yield from self._vm.fire_event_async('device-attach:' + self._bus,
  201. device=device, options=device_assignment.options)
  202. def load_persistent(self, device_assignment: DeviceAssignment):
  203. '''Load DeviceAssignment retrieved from qubes.xml
  204. This can be used only for loading qubes.xml, when VM events are not
  205. enabled yet.
  206. '''
  207. assert not self._vm.events_enabled
  208. assert device_assignment.persistent
  209. device_assignment.bus = self._bus
  210. self._set.add(device_assignment)
  211. def update_persistent(self, device: DeviceInfo, persistent: bool):
  212. '''Update `persistent` flag of already attached device.
  213. '''
  214. if self._vm.is_halted():
  215. raise qubes.exc.QubesVMNotStartedError(self._vm,
  216. 'VM must be running to modify device persistence flag')
  217. assignments = [a for a in self.assignments() if a.device == device]
  218. if not assignments:
  219. raise qubes.exc.QubesValueError('Device not assigned')
  220. assert len(assignments) == 1
  221. assignment = assignments[0]
  222. # be careful to use already present assignment, not the provided one
  223. # - to not change options as a side effect
  224. if persistent and device not in self._set:
  225. assignment.persistent = True
  226. self._set.add(assignment)
  227. elif not persistent and device in self._set:
  228. self._set.discard(assignment)
  229. @asyncio.coroutine
  230. def detach(self, device_assignment: DeviceAssignment):
  231. '''Detach (remove) device from domain.
  232. :param DeviceInfo device: device object
  233. '''
  234. if device_assignment.bus is None:
  235. device_assignment.bus = self._bus
  236. else:
  237. assert device_assignment.bus == self._bus, \
  238. "Trying to attach DeviceAssignment of a different device class"
  239. if device_assignment in self._set and not self._vm.is_halted():
  240. raise qubes.exc.QubesVMNotHaltedError(self._vm,
  241. "Can not remove a persistent attachment from a non halted vm")
  242. if device_assignment not in self.assignments():
  243. raise DeviceNotAttached(
  244. 'device {!s} of class {} not attached to {!s}'.format(
  245. device_assignment.ident, self._bus, self._vm))
  246. device = device_assignment.device
  247. yield from self._vm.fire_event_async('device-pre-detach:' + self._bus,
  248. pre_event=True, device=device)
  249. if device in self._set:
  250. device_assignment.persistent = True
  251. self._set.discard(device_assignment)
  252. yield from self._vm.fire_event_async('device-detach:' + self._bus,
  253. device=device)
  254. def attached(self):
  255. '''List devices which are (or may be) attached to this vm '''
  256. attached = self._vm.fire_event('device-list-attached:' + self._bus,
  257. persistent=None)
  258. if attached:
  259. return [dev for dev, _ in attached]
  260. return []
  261. def persistent(self):
  262. ''' Devices persistently attached and safe to access before libvirt
  263. bootstrap.
  264. '''
  265. return [a.device for a in self._set]
  266. def assignments(self, persistent=None):
  267. '''List assignments for devices which are (or may be) attached to the
  268. vm.
  269. Devices may be attached persistently (so they are included in
  270. :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`,
  271. but be temporarily detached.
  272. :param bool persistent: only include devices which are or are not
  273. attached persistently.
  274. '''
  275. try:
  276. devices = self._vm.fire_event('device-list-attached:' + self._bus,
  277. persistent=persistent)
  278. except Exception: # pylint: disable=broad-except
  279. self._vm.log.exception('Failed to list {} devices'.format(
  280. self._bus))
  281. if persistent is True:
  282. # don't break app.save()
  283. return self._set
  284. raise
  285. result = set()
  286. for dev, options in devices:
  287. if dev in self._set and not persistent:
  288. continue
  289. elif dev in self._set:
  290. result.add(self._set.get(dev))
  291. elif dev not in self._set and persistent:
  292. continue
  293. else:
  294. result.add(
  295. DeviceAssignment(
  296. backend_domain=dev.backend_domain,
  297. ident=dev.ident, options=options,
  298. bus=self._bus))
  299. if persistent is not False:
  300. result.update(self._set)
  301. return result
  302. def available(self):
  303. '''List devices exposed by this vm'''
  304. devices = self._vm.fire_event('device-list:' + self._bus)
  305. return devices
  306. def __iter__(self):
  307. return iter(self.available())
  308. def __getitem__(self, ident):
  309. '''Get device object with given ident.
  310. :returns: py:class:`DeviceInfo`
  311. If domain isn't running, it is impossible to check device validity,
  312. so return UnknownDevice object. Also do the same for non-existing
  313. devices - otherwise it will be impossible to detach already
  314. disconnected device.
  315. :raises AssertionError: when multiple devices with the same ident are
  316. found
  317. '''
  318. dev = self._vm.fire_event('device-get:' + self._bus, ident=ident)
  319. if dev:
  320. assert len(dev) == 1
  321. return dev[0]
  322. return UnknownDevice(self._vm, ident)
  323. class DeviceManager(dict):
  324. '''Device manager that hold all devices by their classess.
  325. :param vm: VM for which we manage devices
  326. '''
  327. def __init__(self, vm):
  328. super(DeviceManager, self).__init__()
  329. self._vm = vm
  330. def __missing__(self, key):
  331. self[key] = DeviceCollection(self._vm, key)
  332. return self[key]
  333. class UnknownDevice(DeviceInfo):
  334. # pylint: disable=too-few-public-methods
  335. '''Unknown device - for example exposed by domain not running currently'''
  336. def __init__(self, backend_domain, ident, description=None,
  337. frontend_domain=None):
  338. if description is None:
  339. description = "Unknown device"
  340. super(UnknownDevice, self).__init__(backend_domain, ident, description,
  341. frontend_domain)
  342. class PersistentCollection:
  343. ''' Helper object managing persistent `DeviceAssignment`s.
  344. '''
  345. def __init__(self):
  346. self._dict = {}
  347. def add(self, assignment: DeviceAssignment):
  348. ''' Add assignment to collection '''
  349. assert assignment.persistent
  350. vm = assignment.backend_domain
  351. ident = assignment.ident
  352. key = (vm, ident)
  353. assert key not in self._dict
  354. self._dict[key] = assignment
  355. def discard(self, assignment):
  356. ''' Discard assignment from collection '''
  357. assert assignment.persistent
  358. vm = assignment.backend_domain
  359. ident = assignment.ident
  360. key = (vm, ident)
  361. if key not in self._dict:
  362. raise KeyError
  363. del self._dict[key]
  364. def __contains__(self, device) -> bool:
  365. return (device.backend_domain, device.ident) in self._dict
  366. def get(self, device: DeviceInfo) -> DeviceAssignment:
  367. ''' Returns the corresponding `qubes.devices.DeviceAssignment` for the
  368. device. '''
  369. return self._dict[(device.backend_domain, device.ident)]
  370. def __iter__(self):
  371. return self._dict.values().__iter__()
  372. def __len__(self) -> int:
  373. return len(self._dict.keys())