qvm_run.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. ''' qvm-run tool'''
  21. import os
  22. import subprocess
  23. import sys
  24. import multiprocessing
  25. import select
  26. import qubesadmin.tools
  27. import qubesadmin.exc
  28. parser = qubesadmin.tools.QubesArgumentParser()
  29. parser.add_argument('--user', '-u', metavar='USER',
  30. help='run command in a qube as USER (available only from dom0)')
  31. parser.add_argument('--autostart', '--auto', '-a',
  32. action='store_true', default=True,
  33. help='option ignored, this is default')
  34. parser.add_argument('--no-autostart', '--no-auto', '-n',
  35. action='store_false', dest='autostart',
  36. help='do not autostart qube')
  37. parser.add_argument('--pass-io', '-p',
  38. action='store_true', dest='passio', default=False,
  39. help='pass stdio from remote program')
  40. parser.add_argument('--localcmd', metavar='COMMAND',
  41. help='with --pass-io, pass stdio to the given program')
  42. parser.add_argument('--gui',
  43. action='store_true', default=True,
  44. help='run the command with GUI (default on)')
  45. parser.add_argument('--no-gui', '--nogui',
  46. action='store_false', dest='gui',
  47. help='run the command without GUI')
  48. parser.add_argument('--colour-output', '--color-output', metavar='COLOUR',
  49. action='store', dest='color_output', default=None,
  50. help='mark the qube output with given ANSI colour (ie. "31" for red)')
  51. parser.add_argument('--colour-stderr', '--color-stderr', metavar='COLOUR',
  52. action='store', dest='color_stderr', default=None,
  53. help='mark the qube stderr with given ANSI colour (ie. "31" for red)')
  54. parser.add_argument('--no-colour-output', '--no-color-output',
  55. action='store_false', dest='color_output',
  56. help='disable colouring the stdio')
  57. parser.add_argument('--no-colour-stderr', '--no-color-stderr',
  58. action='store_false', dest='color_stderr',
  59. help='disable colouring the stderr')
  60. parser.add_argument('--filter-escape-chars',
  61. action='store_true', dest='filter_esc',
  62. default=os.isatty(sys.stdout.fileno()),
  63. help='filter terminal escape sequences (default if output is terminal)')
  64. parser.add_argument('--no-filter-escape-chars',
  65. action='store_false', dest='filter_esc',
  66. help='do not filter terminal escape sequences; DANGEROUS when output is a'
  67. ' terminal emulator')
  68. parser.add_argument('--service',
  69. action='store_true', dest='service',
  70. help='run a qrexec service (named by COMMAND) instead of shell command')
  71. target_parser = parser.add_mutually_exclusive_group()
  72. target_parser.add_argument('--dispvm', action='store', nargs='?',
  73. const=True, metavar='BASE_APPVM',
  74. help='start a service in new Disposable VM; '
  75. 'optionally specify base AppVM for DispVM')
  76. target_parser.add_argument('VMNAME',
  77. nargs='?',
  78. action=qubesadmin.tools.VmNameAction)
  79. # add those manually instead of vmname_args, because of mutually exclusive
  80. # group with --dispvm; parsing is still handled by QubesArgumentParser
  81. target_parser.add_argument('--all', action='store_true', dest='all_domains',
  82. help='run command on all running qubes')
  83. parser.add_argument('--exclude', action='append', default=[],
  84. help='exclude the qube from --all')
  85. parser.add_argument('cmd', metavar='COMMAND',
  86. help='command or service to run')
  87. def copy_stdin(stream):
  88. '''Copy stdin to *stream*'''
  89. # multiprocessing.Process have sys.stdin connected to /dev/null, use fd 0
  90. # directly
  91. while True:
  92. # select so this code works even if fd 0 is non-blocking
  93. select.select([0], [], [])
  94. data = os.read(0, 65536)
  95. if data is None or data == b'':
  96. break
  97. stream.write(data)
  98. stream.flush()
  99. stream.close()
  100. def main(args=None, app=None):
  101. '''Main function of qvm-run tool'''
  102. args = parser.parse_args(args, app=app)
  103. if args.color_output is None and args.filter_esc:
  104. args.color_output = '31'
  105. if args.color_stderr is None and os.isatty(sys.stderr.fileno()):
  106. args.color_stderr = 31
  107. if len(args.domains) > 1 and args.passio and not args.localcmd:
  108. parser.error('--passio cannot be used when more than 1 qube is chosen '
  109. 'and no --localcmd is used')
  110. if args.localcmd and not args.passio:
  111. parser.error('--localcmd have no effect without --pass-io')
  112. if args.color_output and not args.filter_esc:
  113. parser.error('--color-output must be used with --filter-escape-chars')
  114. retcode = 0
  115. run_kwargs = {}
  116. if not args.passio:
  117. run_kwargs['stdout'] = subprocess.DEVNULL
  118. run_kwargs['stderr'] = subprocess.DEVNULL
  119. else:
  120. # connect process output to stdout/err directly if --pass-io is given
  121. run_kwargs['stdout'] = None
  122. run_kwargs['stderr'] = None
  123. if not args.localcmd and args.filter_esc:
  124. run_kwargs['filter_esc'] = True
  125. if isinstance(args.app, qubesadmin.app.QubesLocal) and \
  126. not args.passio and \
  127. not args.localcmd and \
  128. args.service and \
  129. not args.dispvm:
  130. # wait=False works only in dom0; but it's still useful, to save on
  131. # simultaneous vchan connections
  132. run_kwargs['wait'] = False
  133. verbose = args.verbose - args.quiet
  134. if args.passio:
  135. verbose -= 1
  136. # --all and --exclude are handled by QubesArgumentParser
  137. domains = args.domains
  138. dispvm = None
  139. if args.dispvm:
  140. if args.exclude:
  141. parser.error('Cannot use --exclude with --dispvm')
  142. dispvm = qubesadmin.vm.DispVM.from_appvm(args.app,
  143. None if args.dispvm is True else args.dispvm)
  144. domains = [dispvm]
  145. elif args.all_domains:
  146. # --all consider only running VMs
  147. domains = [vm for vm in domains if vm.is_running()]
  148. if args.color_output:
  149. sys.stdout.write('\033[0;{}m'.format(args.color_output))
  150. sys.stdout.flush()
  151. if args.color_stderr:
  152. sys.stderr.write('\033[0;{}m'.format(args.color_stderr))
  153. sys.stderr.flush()
  154. copy_proc = None
  155. try:
  156. procs = []
  157. for vm in domains:
  158. if not args.autostart and not vm.is_running():
  159. continue
  160. try:
  161. if verbose > 0:
  162. if args.color_output:
  163. print('\033[0mRunning \'{}\' on {}\033[0;{}m'.format(
  164. args.cmd, vm.name, args.color_stderr),
  165. file=sys.stderr)
  166. else:
  167. print('Running \'{}\' on {}'.format(args.cmd, vm.name),
  168. file=sys.stderr)
  169. if args.gui and not args.dispvm:
  170. wait_session = vm.run_service('qubes.WaitForSession',
  171. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  172. wait_session.communicate(vm.default_user.encode())
  173. if args.service:
  174. proc = vm.run_service(args.cmd,
  175. user=args.user,
  176. localcmd=args.localcmd,
  177. **run_kwargs)
  178. else:
  179. service = 'qubes.VMShell'
  180. if args.gui and args.dispvm:
  181. service += '+WaitForSession'
  182. proc = vm.run_service(service,
  183. user=args.user,
  184. localcmd=args.localcmd,
  185. **run_kwargs)
  186. proc.stdin.write(vm.prepare_input_for_vmshell(args.cmd))
  187. proc.stdin.flush()
  188. if args.passio and not args.localcmd:
  189. copy_proc = multiprocessing.Process(target=copy_stdin,
  190. args=(proc.stdin,))
  191. copy_proc.start()
  192. # keep the copying process running
  193. proc.stdin.close()
  194. procs.append(proc)
  195. except qubesadmin.exc.QubesException as e:
  196. if args.color_output:
  197. sys.stdout.write('\033[0m')
  198. sys.stdout.flush()
  199. vm.log.error(str(e))
  200. return -1
  201. for proc in procs:
  202. retcode = max(retcode, proc.wait())
  203. finally:
  204. if dispvm:
  205. dispvm.cleanup()
  206. if args.color_output:
  207. sys.stdout.write('\033[0m')
  208. sys.stdout.flush()
  209. if args.color_stderr:
  210. sys.stderr.write('\033[0m')
  211. sys.stderr.flush()
  212. if copy_proc is not None:
  213. copy_proc.terminate()
  214. return retcode
  215. if __name__ == '__main__':
  216. sys.exit(main())