app.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  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. '''
  21. Main Qubes() class and related classes.
  22. '''
  23. import os
  24. import shlex
  25. import socket
  26. import subprocess
  27. import sys
  28. import logging
  29. import qubesadmin.base
  30. import qubesadmin.exc
  31. import qubesadmin.label
  32. import qubesadmin.storage
  33. import qubesadmin.utils
  34. import qubesadmin.vm
  35. import qubesadmin.config
  36. BUF_SIZE = 4096
  37. class VMCollection(object):
  38. '''Collection of VMs objects'''
  39. def __init__(self, app):
  40. self.app = app
  41. self._vm_list = None
  42. self._vm_objects = {}
  43. def clear_cache(self):
  44. '''Clear cached list of VMs'''
  45. self._vm_list = None
  46. def refresh_cache(self, force=False):
  47. '''Refresh cached list of VMs'''
  48. if not force and self._vm_list is not None:
  49. return
  50. vm_list_data = self.app.qubesd_call(
  51. 'dom0',
  52. 'admin.vm.List'
  53. )
  54. new_vm_list = {}
  55. # FIXME: this will probably change
  56. for vm_data in vm_list_data.splitlines():
  57. vm_name, props = vm_data.decode('ascii').split(' ', 1)
  58. vm_name = str(vm_name)
  59. props = props.split(' ')
  60. new_vm_list[vm_name] = dict(
  61. [vm_prop.split('=', 1) for vm_prop in props])
  62. self._vm_list = new_vm_list
  63. for name, vm in list(self._vm_objects.items()):
  64. if vm.name not in self._vm_list:
  65. # VM no longer exists
  66. del self._vm_objects[name]
  67. elif vm.__class__.__name__ != self._vm_list[vm.name]['class']:
  68. # VM class have changed
  69. del self._vm_objects[name]
  70. # TODO: some generation ID, to detect VM re-creation
  71. elif name != vm.name:
  72. # renamed
  73. self._vm_objects[vm.name] = vm
  74. del self._vm_objects[name]
  75. def __getitem__(self, item):
  76. if not self.app.blind_mode and item not in self:
  77. raise KeyError(item)
  78. return self.get_blind(item)
  79. def get_blind(self, item):
  80. '''
  81. Get a vm without downloading the list
  82. and checking if exists
  83. '''
  84. if item not in self._vm_objects:
  85. cls = qubesadmin.vm.QubesVM
  86. # provide class name to constructor, if already cached (which can be
  87. # done by 'item not in self' check above, unless blind_mode is
  88. # enabled
  89. klass = None
  90. if self._vm_list and item in self._vm_list:
  91. klass = self._vm_list[item]['class']
  92. self._vm_objects[item] = cls(self.app, item, klass=klass)
  93. return self._vm_objects[item]
  94. def __contains__(self, item):
  95. self.refresh_cache()
  96. return item in self._vm_list
  97. def __delitem__(self, key):
  98. self.app.qubesd_call(key, 'admin.vm.Remove')
  99. self.clear_cache()
  100. def __iter__(self):
  101. self.refresh_cache()
  102. for vm in sorted(self._vm_list):
  103. yield self[vm]
  104. def keys(self):
  105. '''Get list of VM names.'''
  106. self.refresh_cache()
  107. return self._vm_list.keys()
  108. def values(self):
  109. '''Get list of VM objects.'''
  110. self.refresh_cache()
  111. return [self[name] for name in self._vm_list]
  112. class QubesBase(qubesadmin.base.PropertyHolder):
  113. '''Main Qubes application.
  114. This is a base abstract class, don't use it directly. Use specialized
  115. class in py:class:`qubesadmin.Qubes` instead, which points at
  116. :py:class:`QubesLocal` or :py:class:`QubesRemote`.
  117. '''
  118. #: domains (VMs) collection
  119. domains = None
  120. #: labels collection
  121. labels = None
  122. #: storage pools
  123. pools = None
  124. #: type of qubesd connection: either 'socket' or 'qrexec'
  125. qubesd_connection_type = None
  126. #: logger
  127. log = None
  128. #: do not check for object (VM, label etc) existence before really needed
  129. blind_mode = False
  130. def __init__(self):
  131. super(QubesBase, self).__init__(self, 'admin.property.', 'dom0')
  132. self.domains = VMCollection(self)
  133. self.labels = qubesadmin.base.WrapperObjectsCollection(
  134. self, 'admin.label.List', qubesadmin.label.Label)
  135. self.pools = qubesadmin.base.WrapperObjectsCollection(
  136. self, 'admin.pool.List', qubesadmin.storage.Pool)
  137. #: cache for available storage pool drivers and options to create them
  138. self._pool_drivers = None
  139. self.log = logging.getLogger('app')
  140. def _refresh_pool_drivers(self):
  141. '''
  142. Refresh cached storage pool drivers and their parameters.
  143. :return: None
  144. '''
  145. if self._pool_drivers is None:
  146. pool_drivers_data = self.qubesd_call(
  147. 'dom0', 'admin.pool.ListDrivers', None, None)
  148. assert pool_drivers_data.endswith(b'\n')
  149. pool_drivers = {}
  150. for driver_line in pool_drivers_data.decode('ascii').splitlines():
  151. if not driver_line:
  152. continue
  153. driver_name, driver_options = driver_line.split(' ', 1)
  154. pool_drivers[driver_name] = driver_options.split(' ')
  155. self._pool_drivers = pool_drivers
  156. @property
  157. def pool_drivers(self):
  158. ''' Available storage pool drivers '''
  159. self._refresh_pool_drivers()
  160. return self._pool_drivers.keys()
  161. def pool_driver_parameters(self, driver):
  162. ''' Parameters to initialize storage pool using given driver '''
  163. self._refresh_pool_drivers()
  164. return self._pool_drivers[driver]
  165. def add_pool(self, name, driver, **kwargs):
  166. ''' Add a storage pool to config
  167. :param name: name of storage pool to create
  168. :param driver: driver to use, see :py:meth:`pool_drivers` for
  169. available drivers
  170. :param kwargs: configuration parameters for storage pool,
  171. see :py:meth:`pool_driver_parameters` for a list
  172. '''
  173. # sort parameters only to ease testing, not required by API
  174. payload = 'name={}\n'.format(name) + \
  175. ''.join('{}={}\n'.format(key, value)
  176. for key, value in sorted(kwargs.items()))
  177. self.qubesd_call('dom0', 'admin.pool.Add', driver,
  178. payload.encode('utf-8'))
  179. def remove_pool(self, name):
  180. ''' Remove a storage pool '''
  181. self.qubesd_call('dom0', 'admin.pool.Remove', name, None)
  182. def get_label(self, label):
  183. '''Get label as identified by index or name
  184. :throws KeyError: when label is not found
  185. '''
  186. # first search for name, verbatim
  187. try:
  188. return self.labels[label]
  189. except KeyError:
  190. pass
  191. # then search for index
  192. if isinstance(label, int) or label.isdigit():
  193. for i in self.labels.values():
  194. if i.index == int(label):
  195. return i
  196. raise KeyError(label)
  197. @staticmethod
  198. def get_vm_class(clsname):
  199. '''Find the class for a domain.
  200. Compatibility function, client tools use str to identify domain classes.
  201. :param str clsname: name of the class
  202. :return str: class
  203. '''
  204. return clsname
  205. def add_new_vm(self, cls, name, label, template=None, pool=None,
  206. pools=None):
  207. '''Create new Virtual Machine
  208. Example usage with custom storage pools:
  209. >>> app = qubesadmin.Qubes()
  210. >>> pools = {'private': 'external'}
  211. >>> vm = app.add_new_vm('AppVM', 'my-new-vm', 'red',
  212. >>> 'my-template', pools=pools)
  213. >>> vm.netvm = app.domains['sys-whonix']
  214. :param str cls: name of VM class (`AppVM`, `TemplateVM` etc)
  215. :param str name: name of VM
  216. :param str label: label color for new VM
  217. :param str template: template to use (if apply for given VM class),
  218. can be also VM object; use None for default value
  219. :param str pool: storage pool to use instead of default one
  220. :param dict pools: storage pool for specific volumes
  221. :return new VM object
  222. '''
  223. if not isinstance(cls, str):
  224. cls = cls.__name__
  225. if template is qubesadmin.DEFAULT:
  226. template = None
  227. elif template is not None:
  228. template = str(template)
  229. if pool and pools:
  230. raise ValueError('only one of pool= and pools= can be used')
  231. method_prefix = 'admin.vm.Create.'
  232. payload = 'name={} label={}'.format(name, label)
  233. if pool:
  234. payload += ' pool={}'.format(str(pool))
  235. method_prefix = 'admin.vm.CreateInPool.'
  236. if pools:
  237. payload += ''.join(' pool:{}={}'.format(vol, str(pool))
  238. for vol, pool in sorted(pools.items()))
  239. method_prefix = 'admin.vm.CreateInPool.'
  240. self.qubesd_call('dom0', method_prefix + cls, template,
  241. payload.encode('utf-8'))
  242. self.domains.clear_cache()
  243. return self.domains[name]
  244. def clone_vm(self, src_vm, new_name, new_cls=None,
  245. pool=None, pools=None, ignore_errors=False, ignore_volumes=None):
  246. '''Clone Virtual Machine
  247. Example usage with custom storage pools:
  248. >>> app = qubesadmin.Qubes()
  249. >>> pools = {'private': 'external'}
  250. >>> src_vm = app.domains['personal']
  251. >>> vm = app.clone_vm(src_vm, 'my-new-vm', pools=pools)
  252. >>> vm.label = app.labels['green']
  253. :param QubesVM or str src_vm: source VM
  254. :param str new_name: name of new VM
  255. :param str new_cls: name of VM class (`AppVM`, `TemplateVM` etc) - use
  256. None to copy it from *src_vm*
  257. :param str pool: storage pool to use instead of default one
  258. :param dict pools: storage pool for specific volumes
  259. :param bool ignore_errors: should errors on meta-data setting be only
  260. logged, or abort the whole operation?
  261. :param list ignore_volumes: do not clone volumes on this list,
  262. like 'private' or 'root'
  263. :return new VM object
  264. '''
  265. if pool and pools:
  266. raise ValueError('only one of pool= and pools= can be used')
  267. if isinstance(src_vm, str):
  268. src_vm = self.domains[src_vm]
  269. if new_cls is None:
  270. new_cls = src_vm.klass
  271. template = getattr(src_vm, 'template', None)
  272. if template is not None:
  273. template = str(template)
  274. label = src_vm.label
  275. if pool is None and pools is None:
  276. # use the same pools as the source - check if non default is used
  277. for volume in sorted(src_vm.volumes.values()):
  278. if not volume.save_on_stop:
  279. # clone only persistent volumes
  280. continue
  281. if ignore_volumes and volume.name in ignore_volumes:
  282. continue
  283. default_pool = getattr(self.app, 'default_pool_' + volume.name,
  284. volume.pool)
  285. if default_pool != volume.pool:
  286. if pools is None:
  287. pools = {}
  288. pools[volume.name] = volume.pool
  289. method_prefix = 'admin.vm.Create.'
  290. payload = 'name={} label={}'.format(new_name, label)
  291. if pool:
  292. payload += ' pool={}'.format(str(pool))
  293. method_prefix = 'admin.vm.CreateInPool.'
  294. if pools:
  295. payload += ''.join(' pool:{}={}'.format(vol, str(pool))
  296. for vol, pool in sorted(pools.items()))
  297. method_prefix = 'admin.vm.CreateInPool.'
  298. self.qubesd_call('dom0', method_prefix + new_cls, template,
  299. payload.encode('utf-8'))
  300. self.domains.clear_cache()
  301. dst_vm = self.domains[new_name]
  302. try:
  303. assert isinstance(dst_vm, qubesadmin.vm.QubesVM)
  304. for prop in src_vm.property_list():
  305. # handled by admin.vm.Create call
  306. if prop in ('name', 'qid', 'template', 'label', 'uuid',
  307. 'installed_by_rpm'):
  308. continue
  309. if src_vm.property_is_default(prop):
  310. continue
  311. try:
  312. setattr(dst_vm, prop, getattr(src_vm, prop))
  313. except AttributeError:
  314. pass
  315. except qubesadmin.exc.QubesException as e:
  316. dst_vm.log.error(
  317. 'Failed to set {!s} property: {!s}'.format(prop, e))
  318. if not ignore_errors:
  319. raise
  320. for tag in src_vm.tags:
  321. if tag.startswith('created-by-'):
  322. continue
  323. try:
  324. dst_vm.tags.add(tag)
  325. except qubesadmin.exc.QubesException as e:
  326. dst_vm.log.error(
  327. 'Failed to add {!s} tag: {!s}'.format(tag, e))
  328. if not ignore_errors:
  329. raise
  330. for feature, value in src_vm.features.items():
  331. try:
  332. dst_vm.features[feature] = value
  333. except qubesadmin.exc.QubesException as e:
  334. dst_vm.log.error(
  335. 'Failed to set {!s} feature: {!s}'.format(feature, e))
  336. if not ignore_errors:
  337. raise
  338. try:
  339. dst_vm.firewall.save_rules(src_vm.firewall.rules)
  340. except qubesadmin.exc.QubesException as e:
  341. self.log.error('Failed to set firewall: %s', e)
  342. if not ignore_errors:
  343. raise
  344. try:
  345. # FIXME: convert to qrexec calls to dom0/GUI VM
  346. appmenus_cmd = \
  347. ['qvm-appmenus', '--init', '--update',
  348. '--source', src_vm.name, dst_vm.name]
  349. subprocess.check_output(appmenus_cmd, stderr=subprocess.STDOUT)
  350. except OSError:
  351. # this file needs to be python 2.7 compatible,
  352. # so no FileNotFoundError
  353. self.log.error('Failed to clone appmenus, qvm-appmenus missing')
  354. if not ignore_errors:
  355. raise qubesadmin.exc.QubesException(
  356. 'Failed to clone appmenus')
  357. except subprocess.CalledProcessError as e:
  358. self.log.error('Failed to clone appmenus: %s',
  359. e.output.decode())
  360. if not ignore_errors:
  361. raise qubesadmin.exc.QubesException(
  362. 'Failed to clone appmenus')
  363. except qubesadmin.exc.QubesException:
  364. if not ignore_errors:
  365. del self.domains[dst_vm.name]
  366. raise
  367. try:
  368. for dst_volume in sorted(dst_vm.volumes.values()):
  369. if not dst_volume.save_on_stop:
  370. # clone only persistent volumes
  371. continue
  372. if ignore_volumes and dst_volume.name in ignore_volumes:
  373. continue
  374. src_volume = src_vm.volumes[dst_volume.name]
  375. dst_vm.log.info('Cloning {} volume'.format(dst_volume.name))
  376. dst_volume.clone(src_volume)
  377. except qubesadmin.exc.QubesException:
  378. del self.domains[dst_vm.name]
  379. raise
  380. return dst_vm
  381. def qubesd_call(self, dest, method, arg=None, payload=None,
  382. payload_stream=None):
  383. '''
  384. Execute Admin API method.
  385. Only one of `payload` and `payload_stream` can be specified.
  386. :param dest: Destination VM name
  387. :param method: Full API method name ('admin...')
  388. :param arg: Method argument (if any)
  389. :param payload: Payload send to the method
  390. :param payload_stream: file-like object to read payload from
  391. :return: Data returned by qubesd (string)
  392. .. warning:: *payload_stream* will get closed by this function
  393. '''
  394. raise NotImplementedError(
  395. 'qubesd_call not implemented in QubesBase class; use specialized '
  396. 'class: qubesadmin.Qubes()')
  397. def run_service(self, dest, service, filter_esc=False, user=None,
  398. localcmd=None, wait=True, **kwargs):
  399. '''Run qrexec service in a given destination
  400. *kwargs* are passed verbatim to :py:meth:`subprocess.Popen`.
  401. :param str dest: Destination - may be a VM name or empty
  402. string for default (for a given service)
  403. :param str service: service name
  404. :param bool filter_esc: filter escape sequences to protect terminal \
  405. emulator
  406. :param str user: username to run service as
  407. :param str localcmd: Command to connect stdin/stdout to
  408. :rtype: subprocess.Popen
  409. '''
  410. raise NotImplementedError(
  411. 'run_service not implemented in QubesBase class; use specialized '
  412. 'class: qubesadmin.Qubes()')
  413. class QubesLocal(QubesBase):
  414. '''Application object communicating through local socket.
  415. Used when running in dom0.
  416. '''
  417. qubesd_connection_type = 'socket'
  418. def qubesd_call(self, dest, method, arg=None, payload=None,
  419. payload_stream=None):
  420. '''
  421. Execute Admin API method.
  422. Only one of `payload` and `payload_stream` can be specified.
  423. :param dest: Destination VM name
  424. :param method: Full API method name ('admin...')
  425. :param arg: Method argument (if any)
  426. :param payload: Payload send to the method
  427. :param payload_stream: file-like object to read payload from
  428. :return: Data returned by qubesd (string)
  429. .. warning:: *payload_stream* will get closed by this function
  430. '''
  431. if payload and payload_stream:
  432. raise ValueError(
  433. 'Only one of payload and payload_stream can be used')
  434. if payload_stream:
  435. # payload_stream can be used for large amount of data,
  436. # so optimize for throughput, not latency: spawn actual qrexec
  437. # service implementation, which may use some optimization there (
  438. # see admin.vm.volume.Import - actual data handling is done with dd)
  439. method_path = os.path.join(
  440. qubesadmin.config.QREXEC_SERVICES_DIR, method)
  441. if not os.path.exists(method_path):
  442. raise qubesadmin.exc.QubesDaemonCommunicationError(
  443. '{} not found'.format(method_path))
  444. command = ['env', 'QREXEC_REMOTE_DOMAIN=dom0',
  445. 'QREXEC_REQUESTED_TARGET=' + dest, method_path, arg]
  446. if os.getuid() != 0:
  447. command.insert(0, 'sudo')
  448. proc = subprocess.Popen(command, stdin=payload_stream,
  449. stdout=subprocess.PIPE)
  450. payload_stream.close()
  451. (return_data, _) = proc.communicate()
  452. return self._parse_qubesd_response(return_data)
  453. try:
  454. client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  455. client_socket.connect(qubesadmin.config.QUBESD_SOCKET)
  456. except (IOError, OSError) as e:
  457. raise qubesadmin.exc.QubesDaemonCommunicationError(
  458. 'Failed to connect to qubesd service: %s', str(e))
  459. # src, method, dest, arg
  460. for call_arg in ('dom0', method, dest, arg):
  461. if call_arg is not None:
  462. client_socket.sendall(call_arg.encode('ascii'))
  463. client_socket.sendall(b'\0')
  464. if payload is not None:
  465. client_socket.sendall(payload)
  466. client_socket.shutdown(socket.SHUT_WR)
  467. return_data = client_socket.makefile('rb').read()
  468. client_socket.close()
  469. return self._parse_qubesd_response(return_data)
  470. def run_service(self, dest, service, filter_esc=False, user=None,
  471. localcmd=None, wait=True, **kwargs):
  472. '''Run qrexec service in a given destination
  473. :param str dest: Destination - may be a VM name or empty
  474. string for default (for a given service)
  475. :param str service: service name
  476. :param bool filter_esc: filter escape sequences to protect terminal \
  477. emulator
  478. :param str user: username to run service as
  479. :param str localcmd: Command to connect stdin/stdout to
  480. :param bool wait: wait for remote process to finish
  481. :param int connect_timeout: qrexec client connection timeout
  482. :rtype: subprocess.Popen
  483. '''
  484. if not dest:
  485. raise ValueError('Empty destination name allowed only from a VM')
  486. if not wait and localcmd:
  487. raise ValueError('wait=False incompatible with localcmd')
  488. try:
  489. self.qubesd_call(dest, 'admin.vm.Start')
  490. except qubesadmin.exc.QubesVMNotHaltedError:
  491. pass
  492. qrexec_opts = ['-d', dest]
  493. if filter_esc:
  494. qrexec_opts.extend(['-t'])
  495. if filter_esc or os.isatty(sys.stderr.fileno()):
  496. qrexec_opts.extend(['-T'])
  497. if localcmd:
  498. qrexec_opts.extend(['-l', localcmd])
  499. if user is None:
  500. user = 'DEFAULT'
  501. if not wait:
  502. qrexec_opts.extend(['-e'])
  503. if 'connect_timeout' in kwargs:
  504. qrexec_opts.extend(['-w', str(kwargs.pop('connect_timeout'))])
  505. kwargs.setdefault('stdin', subprocess.PIPE)
  506. kwargs.setdefault('stdout', subprocess.PIPE)
  507. kwargs.setdefault('stderr', subprocess.PIPE)
  508. proc = subprocess.Popen([qubesadmin.config.QREXEC_CLIENT] +
  509. qrexec_opts + ['{}:QUBESRPC {} dom0'.format(user, service)],
  510. **kwargs)
  511. return proc
  512. class QubesRemote(QubesBase):
  513. '''Application object communicating through qrexec services.
  514. Used when running in VM.
  515. '''
  516. qubesd_connection_type = 'qrexec'
  517. def qubesd_call(self, dest, method, arg=None, payload=None,
  518. payload_stream=None):
  519. '''
  520. Execute Admin API method.
  521. Only one of `payload` and `payload_stream` can be specified.
  522. :param dest: Destination VM name
  523. :param method: Full API method name ('admin...')
  524. :param arg: Method argument (if any)
  525. :param payload: Payload send to the method
  526. :param payload_stream: file-like object to read payload from
  527. :return: Data returned by qubesd (string)
  528. .. warning:: *payload_stream* will get closed by this function
  529. '''
  530. if payload and payload_stream:
  531. raise ValueError(
  532. 'Only one of payload and payload_stream can be used')
  533. service_name = method
  534. if arg is not None:
  535. service_name += '+' + arg
  536. p = subprocess.Popen([qubesadmin.config.QREXEC_CLIENT_VM,
  537. dest, service_name],
  538. stdin=(payload_stream or subprocess.PIPE),
  539. stdout=subprocess.PIPE,
  540. stderr=subprocess.PIPE)
  541. if payload_stream is not None:
  542. payload_stream.close()
  543. (stdout, stderr) = p.communicate(payload)
  544. if p.returncode != 0:
  545. raise qubesadmin.exc.QubesDaemonNoResponseError(
  546. 'Service call error: %s', stderr.decode())
  547. return self._parse_qubesd_response(stdout)
  548. def run_service(self, dest, service, filter_esc=False, user=None,
  549. localcmd=None, wait=True, **kwargs):
  550. '''Run qrexec service in a given destination
  551. :param str dest: Destination - may be a VM name or empty
  552. string for default (for a given service)
  553. :param str service: service name
  554. :param bool filter_esc: filter escape sequences to protect terminal \
  555. emulator
  556. :param str user: username to run service as
  557. :param str localcmd: Command to connect stdin/stdout to
  558. :param bool wait: wait for process to finish
  559. :rtype: subprocess.Popen
  560. '''
  561. if filter_esc:
  562. raise NotImplementedError(
  563. 'filter_esc not implemented for calls from VM')
  564. if user:
  565. raise ValueError(
  566. 'non-default user not possible for calls from VM')
  567. if not wait and localcmd:
  568. raise ValueError('wait=False incompatible with localcmd')
  569. if not wait:
  570. # qrexec-client-vm can only request service calls, which are
  571. # started using MSG_EXEC_CMDLINE qrexec protocol message; this
  572. # message means "start the process, pipe its stdin/out/err,
  573. # and when it terminates, send exit code back".
  574. # According to the protocol qrexec-client-vm needs to wait for
  575. # MSG_DATA_EXIT_CODE, so implementing wait=False would require
  576. # some protocol change (or protocol violation).
  577. raise NotImplementedError(
  578. 'wait=False not implemented for calls from VM')
  579. kwargs.setdefault('stdin', subprocess.PIPE)
  580. kwargs.setdefault('stdout', subprocess.PIPE)
  581. kwargs.setdefault('stderr', subprocess.PIPE)
  582. proc = subprocess.Popen([qubesadmin.config.QREXEC_CLIENT_VM,
  583. dest or '', service] + (shlex.split(localcmd) if localcmd else []),
  584. **kwargs)
  585. return proc