qvm_device.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. for dev in domain.devices[args.devclass].available():
  77. devices.add(dev)
  78. except qubesadmin.exc.QubesDaemonAccessError:
  79. raise qubesadmin.exc.QubesException(
  80. "Failed to list '%s' devices, this device type either "
  81. "does not exist or you do not have access to it.", args.devclass)
  82. result = {dev: Line(dev) for dev in devices}
  83. for dev in result:
  84. for domain in app.domains:
  85. if domain == dev.backend_domain:
  86. continue
  87. for assignment in domain.devices[args.devclass].assignments():
  88. if dev != assignment:
  89. continue
  90. if assignment.options:
  91. result[dev].frontends.append('{!s} ({})'.format(
  92. domain, ', '.join('{}={}'.format(key, value)
  93. for key, value in
  94. assignment.options.items())))
  95. else:
  96. result[dev].frontends.append(str(domain))
  97. qubesadmin.tools.print_table(prepare_table(result.values()))
  98. def attach_device(args):
  99. """ Called by the parser to execute the :program:`qvm-devices attach`
  100. subcommand.
  101. """
  102. device_assignment = args.device_assignment
  103. vm = args.domains[0]
  104. options = dict(opt.split('=', 1) for opt in args.option or [])
  105. if args.ro:
  106. options['read-only'] = 'yes'
  107. device_assignment.persistent = args.persistent
  108. device_assignment.options = options
  109. vm.devices[args.devclass].attach(device_assignment)
  110. def detach_device(args):
  111. """ Called by the parser to execute the :program:`qvm-devices detach`
  112. subcommand.
  113. """
  114. vm = args.domains[0]
  115. if args.device_assignment:
  116. vm.devices[args.devclass].detach(args.device_assignment)
  117. else:
  118. for device_assignment in vm.devices[args.devclass].assignments():
  119. vm.devices[args.devclass].detach(device_assignment)
  120. def init_list_parser(sub_parsers):
  121. """ Configures the parser for the :program:`qvm-devices list` subcommand """
  122. # pylint: disable=protected-access
  123. list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'),
  124. help='list devices')
  125. vm_name_group = qubesadmin.tools.VmNameGroup(
  126. list_parser, required=False, vm_action=qubesadmin.tools.VmNameAction,
  127. help='list devices assigned to specific domain(s)')
  128. list_parser._mutually_exclusive_groups.append(vm_name_group)
  129. list_parser.set_defaults(func=list_devices)
  130. class DeviceAction(qubesadmin.tools.QubesAction):
  131. """ Action for argument parser that gets the
  132. :py:class:``qubesadmin.device.DeviceAssignment`` from a
  133. BACKEND:DEVICE_ID string.
  134. """ # pylint: disable=too-few-public-methods
  135. def __init__(self, help='A backend & device id combination',
  136. required=True, allow_unknown=False, **kwargs):
  137. # pylint: disable=redefined-builtin
  138. self.allow_unknown = allow_unknown
  139. super().__init__(help=help, required=required,
  140. **kwargs)
  141. def __call__(self, parser, namespace, values, option_string=None):
  142. """ Set ``namespace.device_assignment`` to ``values`` """
  143. setattr(namespace, self.dest, values)
  144. def parse_qubes_app(self, parser, namespace):
  145. app = namespace.app
  146. backend_device_id = getattr(namespace, self.dest)
  147. devclass = namespace.devclass
  148. if backend_device_id is None:
  149. return
  150. try:
  151. vmname, device_id = backend_device_id.split(':', 1)
  152. vm = None
  153. try:
  154. vm = app.domains[vmname]
  155. except KeyError:
  156. parser.error_runtime("no backend vm {!r}".format(vmname))
  157. try:
  158. dev = vm.devices[devclass][device_id]
  159. if not self.allow_unknown and \
  160. isinstance(dev, qubesadmin.devices.UnknownDevice):
  161. raise KeyError(device_id)
  162. except KeyError:
  163. parser.error_runtime(
  164. "backend vm {!r} doesn't expose device {!r}".format(
  165. vmname, device_id))
  166. device_assignment = qubesadmin.devices.DeviceAssignment(
  167. vm, device_id)
  168. setattr(namespace, self.dest, device_assignment)
  169. except ValueError:
  170. parser.error(
  171. 'expected a backend vm & device id combination like foo:bar '
  172. 'got %s' % backend_device_id)
  173. def get_parser(device_class=None):
  174. """Create :py:class:`argparse.ArgumentParser` suitable for
  175. :program:`qvm-block`.
  176. """
  177. parser = qubesadmin.tools.QubesArgumentParser(description=__doc__)
  178. parser.register('action', 'parsers',
  179. qubesadmin.tools.AliasedSubParsersAction)
  180. parser.allow_abbrev = False
  181. if device_class:
  182. parser.add_argument('devclass', const=device_class,
  183. action='store_const',
  184. help=argparse.SUPPRESS)
  185. else:
  186. parser.add_argument('devclass', metavar='DEVICE_CLASS', action='store',
  187. help="Device class to manage ('pci', 'usb', etc)")
  188. # default action
  189. parser.set_defaults(func=list_devices)
  190. sub_parsers = parser.add_subparsers(
  191. title='commands',
  192. description="For more information see qvm-device command -h",
  193. dest='command')
  194. init_list_parser(sub_parsers)
  195. attach_parser = sub_parsers.add_parser(
  196. 'attach', help="Attach device to domain", aliases=('at', 'a'))
  197. detach_parser = sub_parsers.add_parser(
  198. "detach", help="Detach device from domain", aliases=('d', 'dt'))
  199. attach_parser.add_argument('VMNAME', nargs=1,
  200. action=qubesadmin.tools.VmNameAction)
  201. detach_parser.add_argument('VMNAME', nargs=1,
  202. action=qubesadmin.tools.VmNameAction)
  203. attach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
  204. dest='device_assignment',
  205. action=DeviceAction)
  206. detach_parser.add_argument(metavar='BACKEND:DEVICE_ID',
  207. dest='device_assignment',
  208. nargs=argparse.OPTIONAL,
  209. action=DeviceAction, allow_unknown=True)
  210. attach_parser.add_argument('--option', '-o', action='append',
  211. help="Set option for the device in opt=value "
  212. "form (can be specified "
  213. "multiple times), see man qvm-device for "
  214. "details")
  215. attach_parser.add_argument('--ro', action='store_true', default=False,
  216. help="Attach device read-only (alias for "
  217. "read-only=yes option, "
  218. "takes precedence)")
  219. attach_parser.add_argument('--persistent', '-p', action='store_true',
  220. default=False,
  221. help="Attach device persistently (so it will "
  222. "be automatically "
  223. "attached at qube startup)")
  224. attach_parser.set_defaults(func=attach_device)
  225. detach_parser.set_defaults(func=detach_device)
  226. parser.add_argument('--list-device-classes', action='store_true',
  227. default=False)
  228. return parser
  229. def main(args=None, app=None):
  230. """Main routine of :program:`qvm-block`."""
  231. basename = os.path.basename(sys.argv[0])
  232. devclass = None
  233. if basename.startswith('qvm-') and basename != 'qvm-device':
  234. devclass = basename[4:]
  235. args = get_parser(devclass).parse_args(args, app=app)
  236. if args.list_device_classes:
  237. print('\n'.join(qubesadmin.Qubes().list_deviceclass()))
  238. return 0
  239. try:
  240. args.func(args)
  241. except qubesadmin.exc.QubesException as e:
  242. print(str(e), file=sys.stderr)
  243. return 1
  244. return 0
  245. if __name__ == '__main__':
  246. # Special treatment for '--list-device-classes' (alias --list-classes)
  247. curr_action = sys.argv[1:]
  248. if set(curr_action).intersection(
  249. {'--list-device-classes', '--list-classes'}):
  250. sys.exit(main(args=['', '--list-device-classes']))
  251. sys.exit(main())