qvm_device.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. # encoding=utf-8
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  6. # Copyright (C) 2016 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU Lesser General Public License as published by
  11. # the Free Software Foundation; either version 2.1 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU Lesser General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Lesser General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. """Qubes volume and block device managment"""
  23. import argparse
  24. import os
  25. import sys
  26. import qubesadmin
  27. import qubesadmin.exc
  28. import qubesadmin.tools
  29. import qubesadmin.devices
  30. def prepare_table(dev_list):
  31. """ Converts a list of :py:class:`qubes.devices.DeviceInfo` objects to a
  32. list of tupples for the :py:func:`qubes.tools.print_table`.
  33. If :program:`qvm-devices` is running in a TTY, it will ommit duplicate
  34. data.
  35. :param iterable dev_list: List of :py:class:`qubes.devices.DeviceInfo`
  36. objects.
  37. :returns: list of tupples
  38. """
  39. output = []
  40. header = []
  41. if sys.stdout.isatty():
  42. header += [('BACKEND:DEVID', 'DESCRIPTION', 'USED BY')] # NOQA
  43. for line in dev_list:
  44. output += [(
  45. line.ident,
  46. line.description,
  47. str(line.assignments),
  48. )]
  49. return header + sorted(output)
  50. class Line(object):
  51. """Helper class to hold single device info for listing"""
  52. # pylint: disable=too-few-public-methods
  53. def __init__(self, device: qubesadmin.devices.DeviceInfo, attached_to=None):
  54. self.ident = "{!s}:{!s}".format(device.backend_domain, device.ident)
  55. self.description = device.description
  56. self.attached_to = attached_to if attached_to else ""
  57. self.frontends = []
  58. @property
  59. def assignments(self):
  60. """list of frontends the device is assigned to"""
  61. return ', '.join(self.frontends)
  62. def list_devices(args):
  63. """ Called by the parser to execute the qubes-devices list
  64. subcommand. """
  65. app = args.app
  66. devices = set()
  67. try:
  68. if hasattr(args, 'domains') and args.domains:
  69. for domain in args.domains:
  70. for dev in domain.devices[args.devclass].attached():
  71. devices.add(dev)
  72. for dev in domain.devices[args.devclass].available():
  73. devices.add(dev)
  74. else:
  75. for domain in app.domains:
  76. try:
  77. for dev in domain.devices[args.devclass].available():
  78. devices.add(dev)
  79. except qubesadmin.exc.QubesVMNotFoundError:
  80. continue
  81. except qubesadmin.exc.QubesDaemonAccessError:
  82. raise qubesadmin.exc.QubesException(
  83. "Failed to list '%s' devices, this device type either "
  84. "does not exist or you do not have access to it.", args.devclass)
  85. result = {dev: Line(dev) for dev in devices}
  86. for dev in result:
  87. for domain in app.domains:
  88. if domain == dev.backend_domain:
  89. continue
  90. try:
  91. for assignment in domain.devices[args.devclass].assignments():
  92. if dev != assignment:
  93. continue
  94. if assignment.options:
  95. result[dev].frontends.append('{!s} ({})'.format(
  96. domain, ', '.join('{}={}'.format(key, value)
  97. for key, value in
  98. assignment.options.items())))
  99. else:
  100. result[dev].frontends.append(str(domain))
  101. except qubesadmin.exc.QubesVMNotFoundError:
  102. continue
  103. qubesadmin.tools.print_table(prepare_table(result.values()))
  104. def attach_device(args):
  105. """ Called by the parser to execute the :program:`qvm-devices attach`
  106. subcommand.
  107. """
  108. device_assignment = args.device_assignment
  109. vm = args.domains[0]
  110. options = dict(opt.split('=', 1) for opt in args.option or [])
  111. if args.ro:
  112. options['read-only'] = 'yes'
  113. device_assignment.persistent = args.persistent
  114. device_assignment.options = options
  115. vm.devices[args.devclass].attach(device_assignment)
  116. def detach_device(args):
  117. """ Called by the parser to execute the :program:`qvm-devices detach`
  118. subcommand.
  119. """
  120. vm = args.domains[0]
  121. if args.device_assignment:
  122. vm.devices[args.devclass].detach(args.device_assignment)
  123. else:
  124. for device_assignment in vm.devices[args.devclass].assignments():
  125. vm.devices[args.devclass].detach(device_assignment)
  126. def init_list_parser(sub_parsers):
  127. """ Configures the parser for the :program:`qvm-devices list` subcommand """
  128. # pylint: disable=protected-access
  129. list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'),
  130. help='list devices')
  131. vm_name_group = qubesadmin.tools.VmNameGroup(
  132. list_parser, required=False, vm_action=qubesadmin.tools.VmNameAction,
  133. help='list devices assigned to specific domain(s)')
  134. list_parser._mutually_exclusive_groups.append(vm_name_group)
  135. list_parser.set_defaults(func=list_devices)
  136. class DeviceAction(qubesadmin.tools.QubesAction):
  137. """ Action for argument parser that gets the
  138. :py:class:``qubesadmin.device.DeviceAssignment`` from a
  139. BACKEND:DEVICE_ID string.
  140. """ # pylint: disable=too-few-public-methods
  141. def __init__(self, help='A backend & device id combination',
  142. required=True, allow_unknown=False, **kwargs):
  143. # pylint: disable=redefined-builtin
  144. self.allow_unknown = allow_unknown
  145. super().__init__(help=help, required=required,
  146. **kwargs)
  147. def __call__(self, parser, namespace, values, option_string=None):
  148. """ Set ``namespace.device_assignment`` to ``values`` """
  149. setattr(namespace, self.dest, values)
  150. def parse_qubes_app(self, parser, namespace):
  151. app = namespace.app
  152. backend_device_id = getattr(namespace, self.dest)
  153. devclass = namespace.devclass
  154. if backend_device_id is None:
  155. return
  156. try:
  157. vmname, device_id = backend_device_id.split(':', 1)
  158. vm = None
  159. try:
  160. vm = app.domains[vmname]
  161. except KeyError:
  162. parser.error_runtime("no backend vm {!r}".format(vmname))
  163. try:
  164. dev = vm.devices[devclass][device_id]
  165. if not self.allow_unknown and \
  166. isinstance(dev, qubesadmin.devices.UnknownDevice):
  167. raise KeyError(device_id)
  168. except KeyError:
  169. parser.error_runtime(
  170. "backend vm {!r} doesn't expose device {!r}".format(
  171. vmname, device_id))
  172. device_assignment = qubesadmin.devices.DeviceAssignment(
  173. vm, device_id)
  174. setattr(namespace, self.dest, device_assignment)
  175. except ValueError:
  176. parser.error(
  177. 'expected a backend vm & device id combination like foo:bar '
  178. 'got %s' % backend_device_id)
  179. def get_parser(device_class=None):
  180. """Create :py:class:`argparse.ArgumentParser` suitable for
  181. :program:`qvm-block`.
  182. """
  183. parser = qubesadmin.tools.QubesArgumentParser(description=__doc__)
  184. parser.register('action', 'parsers',
  185. qubesadmin.tools.AliasedSubParsersAction)
  186. parser.allow_abbrev = False
  187. if device_class:
  188. parser.add_argument('devclass', const=device_class,
  189. action='store_const',
  190. help=argparse.SUPPRESS)
  191. else:
  192. parser.add_argument('devclass', metavar='DEVICE_CLASS', action='store',
  193. help="Device class to manage ('pci', 'usb', etc)")
  194. # default action
  195. parser.set_defaults(func=list_devices)
  196. sub_parsers = parser.add_subparsers(
  197. title='commands',
  198. description="For more information see qvm-device command -h",
  199. dest='command')
  200. init_list_parser(sub_parsers)
  201. attach_parser = sub_parsers.add_parser(
  202. 'attach', help="Attach device to domain", aliases=('at', 'a'))
  203. detach_parser = sub_parsers.add_parser(
  204. "detach", help="Detach device from domain", aliases=('d', 'dt'))
  205. attach_parser.add_argument('VMNAME', nargs=1,
  206. action=qubesadmin.tools.VmNameAction)
  207. detach_parser.add_argument('VMNAME', nargs=1,
  208. action=qubesadmin.tools.VmNameAction)
  209. attach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
  210. dest='device_assignment',
  211. action=DeviceAction)
  212. detach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
  213. dest='device_assignment',
  214. nargs=argparse.OPTIONAL,
  215. action=DeviceAction, allow_unknown=True)
  216. attach_parser.add_argument('--option', '-o', action='append',
  217. help="Set option for the device in opt=value "
  218. "form (can be specified "
  219. "multiple times), see man qvm-device for "
  220. "details")
  221. attach_parser.add_argument('--ro', action='store_true', default=False,
  222. help="Attach device read-only (alias for "
  223. "read-only=yes option, "
  224. "takes precedence)")
  225. attach_parser.add_argument('--persistent', '-p', action='store_true',
  226. default=False,
  227. help="Attach device persistently (so it will "
  228. "be automatically "
  229. "attached at qube startup)")
  230. attach_parser.set_defaults(func=attach_device)
  231. detach_parser.set_defaults(func=detach_device)
  232. parser.add_argument('--list-device-classes', action='store_true',
  233. default=False)
  234. return parser
  235. def main(args=None, app=None):
  236. """Main routine of :program:`qvm-block`."""
  237. basename = os.path.basename(sys.argv[0])
  238. devclass = None
  239. if basename.startswith('qvm-') and basename != 'qvm-device':
  240. devclass = basename[4:]
  241. parser = get_parser(devclass)
  242. args = parser.parse_args(args, app=app)
  243. if args.list_device_classes:
  244. print('\n'.join(qubesadmin.Qubes().list_deviceclass()))
  245. return 0
  246. try:
  247. args.func(args)
  248. except qubesadmin.exc.QubesException as e:
  249. parser.print_error(str(e))
  250. return 1
  251. return 0
  252. if __name__ == '__main__':
  253. # Special treatment for '--list-device-classes' (alias --list-classes)
  254. curr_action = sys.argv[1:]
  255. if set(curr_action).intersection(
  256. {'--list-device-classes', '--list-classes'}):
  257. sys.exit(main(args=['', '--list-device-classes']))
  258. sys.exit(main())