app.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. # -*- encoding: utf-8 -*-
  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 grp
  24. import os
  25. import shlex
  26. import socket
  27. import shutil
  28. import subprocess
  29. import sys
  30. import logging
  31. import qubesadmin.base
  32. import qubesadmin.exc
  33. import qubesadmin.label
  34. import qubesadmin.storage
  35. import qubesadmin.utils
  36. import qubesadmin.vm
  37. import qubesadmin.config
  38. import qubesadmin.devices
  39. class VMCollection(object):
  40. """Collection of VMs objects"""
  41. def __init__(self, app):
  42. self.app = app
  43. self._vm_list = None
  44. self._vm_objects = {}
  45. def clear_cache(self):
  46. """Clear cached list of VMs"""
  47. self._vm_list = None
  48. def refresh_cache(self, force=False):
  49. """Refresh cached list of VMs"""
  50. if not force and self._vm_list is not None:
  51. return
  52. vm_list_data = self.app.qubesd_call(
  53. 'dom0',
  54. 'admin.vm.List'
  55. )
  56. new_vm_list = {}
  57. # FIXME: this will probably change
  58. for vm_data in vm_list_data.splitlines():
  59. vm_name, props = vm_data.decode('ascii').split(' ', 1)
  60. vm_name = str(vm_name)
  61. props = props.split(' ')
  62. new_vm_list[vm_name] = dict(
  63. [vm_prop.split('=', 1) for vm_prop in props])
  64. # if cache not enabled, drop power state
  65. if not self.app.cache_enabled:
  66. try:
  67. del new_vm_list[vm_name]['state']
  68. except KeyError:
  69. pass
  70. self._vm_list = new_vm_list
  71. for name, vm in list(self._vm_objects.items()):
  72. if vm.name not in self._vm_list:
  73. # VM no longer exists
  74. del self._vm_objects[name]
  75. elif vm.klass != self._vm_list[vm.name]['class']:
  76. # VM class have changed
  77. del self._vm_objects[name]
  78. # TODO: some generation ID, to detect VM re-creation
  79. elif name != vm.name:
  80. # renamed
  81. self._vm_objects[vm.name] = vm
  82. del self._vm_objects[name]
  83. def __getitem__(self, item):
  84. if isinstance(item, qubesadmin.vm.QubesVM):
  85. item = item.name
  86. if not self.app.blind_mode and item not in self:
  87. raise KeyError(item)
  88. return self.get_blind(item)
  89. def get_blind(self, item):
  90. """
  91. Get a vm without downloading the list
  92. and checking if exists
  93. """
  94. if item not in self._vm_objects:
  95. cls = qubesadmin.vm.QubesVM
  96. # provide class name to constructor, if already cached (which can be
  97. # done by 'item not in self' check above, unless blind_mode is
  98. # enabled
  99. klass = None
  100. power_state = None
  101. if self._vm_list and item in self._vm_list:
  102. klass = self._vm_list[item]['class']
  103. power_state = self._vm_list[item].get('state')
  104. self._vm_objects[item] = cls(self.app, item, klass=klass,
  105. power_state=power_state)
  106. return self._vm_objects[item]
  107. def __contains__(self, item):
  108. if isinstance(item, qubesadmin.vm.QubesVM):
  109. item = item.name
  110. self.refresh_cache()
  111. return item in self._vm_list
  112. def __delitem__(self, key):
  113. self.app.qubesd_call(key, 'admin.vm.Remove')
  114. self.clear_cache()
  115. def __iter__(self):
  116. self.refresh_cache()
  117. for vm in sorted(self._vm_list):
  118. yield self[vm]
  119. def keys(self):
  120. """Get list of VM names."""
  121. self.refresh_cache()
  122. return self._vm_list.keys()
  123. def values(self):
  124. """Get list of VM objects."""
  125. self.refresh_cache()
  126. return [self[name] for name in self._vm_list]
  127. class QubesBase(qubesadmin.base.PropertyHolder):
  128. """Main Qubes application.
  129. This is a base abstract class, don't use it directly. Use specialized
  130. class in py:class:`qubesadmin.Qubes` instead, which points at
  131. :py:class:`QubesLocal` or :py:class:`QubesRemote`.
  132. """
  133. #: domains (VMs) collection
  134. domains = None
  135. #: labels collection
  136. labels = None
  137. #: storage pools
  138. pools = None
  139. #: type of qubesd connection: either 'socket' or 'qrexec'
  140. qubesd_connection_type = None
  141. #: logger
  142. log = None
  143. #: do not check for object (VM, label etc) existence before really needed
  144. blind_mode = False
  145. #: cache retrieved properties values
  146. cache_enabled = False
  147. def __init__(self):
  148. super(QubesBase, self).__init__(self, 'admin.property.', 'dom0')
  149. self.domains = VMCollection(self)
  150. self.labels = qubesadmin.base.WrapperObjectsCollection(
  151. self, 'admin.label.List', qubesadmin.label.Label)
  152. self.pools = qubesadmin.base.WrapperObjectsCollection(
  153. self, 'admin.pool.List', qubesadmin.storage.Pool)
  154. #: cache for available storage pool drivers and options to create them
  155. self._pool_drivers = None
  156. self.log = logging.getLogger('app')
  157. self._local_name = None
  158. def list_vmclass(self):
  159. """Call Qubesd in order to obtain the vm classes list"""
  160. vmclass = self.qubesd_call('dom0', 'admin.vmclass.List') \
  161. .decode().splitlines()
  162. return sorted(vmclass)
  163. def list_deviceclass(self):
  164. """Call Qubesd in order to obtain the device classes list"""
  165. deviceclasses = self.qubesd_call('dom0', 'admin.deviceclass.List') \
  166. .decode().splitlines()
  167. return sorted(deviceclasses)
  168. def _refresh_pool_drivers(self):
  169. """
  170. Refresh cached storage pool drivers and their parameters.
  171. :return: None
  172. """
  173. if self._pool_drivers is None:
  174. pool_drivers_data = self.qubesd_call(
  175. 'dom0', 'admin.pool.ListDrivers', None, None)
  176. assert pool_drivers_data.endswith(b'\n')
  177. pool_drivers = {}
  178. for driver_line in pool_drivers_data.decode('ascii').splitlines():
  179. if not driver_line:
  180. continue
  181. driver_name, driver_options = driver_line.split(' ', 1)
  182. pool_drivers[driver_name] = driver_options.split(' ')
  183. self._pool_drivers = pool_drivers
  184. @property
  185. def pool_drivers(self):
  186. """ Available storage pool drivers """
  187. self._refresh_pool_drivers()
  188. return self._pool_drivers.keys()
  189. def pool_driver_parameters(self, driver):
  190. """ Parameters to initialize storage pool using given driver """
  191. self._refresh_pool_drivers()
  192. return self._pool_drivers[driver]
  193. def add_pool(self, name, driver, **kwargs):
  194. """ Add a storage pool to config
  195. :param name: name of storage pool to create
  196. :param driver: driver to use, see :py:meth:`pool_drivers` for
  197. available drivers
  198. :param kwargs: configuration parameters for storage pool,
  199. see :py:meth:`pool_driver_parameters` for a list
  200. """
  201. # sort parameters only to ease testing, not required by API
  202. payload = 'name={}\n'.format(name) + \
  203. ''.join('{}={}\n'.format(key, value)
  204. for key, value in sorted(kwargs.items()))
  205. self.qubesd_call('dom0', 'admin.pool.Add', driver,
  206. payload.encode('utf-8'))
  207. def remove_pool(self, name):
  208. """ Remove a storage pool """
  209. self.qubesd_call('dom0', 'admin.pool.Remove', name, None)
  210. @property
  211. def local_name(self):
  212. """ Get localhost name """
  213. if not self._local_name:
  214. self._local_name = os.uname()[1]
  215. return self._local_name
  216. def get_label(self, label):
  217. """Get label as identified by index or name
  218. :throws KeyError: when label is not found
  219. """
  220. # first search for name, verbatim
  221. try:
  222. return self.labels[label]
  223. except KeyError:
  224. pass
  225. # then search for index
  226. if isinstance(label, int) or label.isdigit():
  227. for i in self.labels.values():
  228. if i.index == int(label):
  229. return i
  230. raise KeyError(label)
  231. @staticmethod
  232. def get_vm_class(clsname):
  233. """Find the class for a domain.
  234. Compatibility function, client tools use str to identify domain classes.
  235. :param str clsname: name of the class
  236. :return str: class
  237. """
  238. return clsname
  239. def add_new_vm(self, cls, name, label, template=None, pool=None,
  240. pools=None):
  241. """Create new Virtual Machine
  242. Example usage with custom storage pools:
  243. >>> app = qubesadmin.Qubes()
  244. >>> pools = {'private': 'external'}
  245. >>> vm = app.add_new_vm('AppVM', 'my-new-vm', 'red',
  246. >>> 'my-template', pools=pools)
  247. >>> vm.netvm = app.domains['sys-whonix']
  248. :param str cls: name of VM class (`AppVM`, `TemplateVM` etc)
  249. :param str name: name of VM
  250. :param str label: label color for new VM
  251. :param str template: template to use (if apply for given VM class),
  252. can be also VM object; use None for default value
  253. :param str pool: storage pool to use instead of default one
  254. :param dict pools: storage pool for specific volumes
  255. :return new VM object
  256. """
  257. if not isinstance(cls, str):
  258. cls = cls.__name__
  259. if template is qubesadmin.DEFAULT:
  260. template = None
  261. elif template is not None:
  262. template = str(template)
  263. if pool and pools:
  264. raise ValueError('only one of pool= and pools= can be used')
  265. method_prefix = 'admin.vm.Create.'
  266. payload = 'name={} label={}'.format(name, label)
  267. if pool:
  268. payload += ' pool={}'.format(str(pool))
  269. method_prefix = 'admin.vm.CreateInPool.'
  270. if pools:
  271. payload += ''.join(' pool:{}={}'.format(vol, str(pool))
  272. for vol, pool in sorted(pools.items()))
  273. method_prefix = 'admin.vm.CreateInPool.'
  274. self.qubesd_call('dom0', method_prefix + cls, template,
  275. payload.encode('utf-8'))
  276. self.domains.clear_cache()
  277. return self.domains[name]
  278. def clone_vm(self, src_vm, new_name, new_cls=None, pool=None, pools=None,
  279. ignore_errors=False, ignore_volumes=None,
  280. ignore_devices=False):
  281. # pylint: disable=too-many-statements
  282. """Clone Virtual Machine
  283. Example usage with custom storage pools:
  284. >>> app = qubesadmin.Qubes()
  285. >>> pools = {'private': 'external'}
  286. >>> src_vm = app.domains['personal']
  287. >>> vm = app.clone_vm(src_vm, 'my-new-vm', pools=pools)
  288. >>> vm.label = app.labels['green']
  289. :param QubesVM or str src_vm: source VM
  290. :param str new_name: name of new VM
  291. :param str new_cls: name of VM class (`AppVM`, `TemplateVM` etc) - use
  292. None to copy it from *src_vm*
  293. :param str pool: storage pool to use instead of default one
  294. :param dict pools: storage pool for specific volumes
  295. :param bool ignore_errors: should errors on meta-data setting be only
  296. logged, or abort the whole operation?
  297. :param list ignore_volumes: do not clone volumes on this list,
  298. like 'private' or 'root'
  299. :param bool ignore_devices: if True, do not copy device assignments
  300. :return new VM object
  301. """
  302. if pool and pools:
  303. raise ValueError('only one of pool= and pools= can be used')
  304. if isinstance(src_vm, str):
  305. src_vm = self.domains[src_vm]
  306. if new_cls is None:
  307. new_cls = src_vm.klass
  308. template = getattr(src_vm, 'template', None)
  309. if template is not None:
  310. template = str(template)
  311. label = src_vm.label
  312. if pool is None and pools is None:
  313. # use the same pools as the source - check if non default is used
  314. for volume in sorted(src_vm.volumes.values()):
  315. if not volume.save_on_stop:
  316. # clone only persistent volumes
  317. continue
  318. if ignore_volumes and volume.name in ignore_volumes:
  319. continue
  320. default_pool = getattr(self.app, 'default_pool_' + volume.name,
  321. volume.pool)
  322. if default_pool != volume.pool:
  323. if pools is None:
  324. pools = {}
  325. pools[volume.name] = volume.pool
  326. method_prefix = 'admin.vm.Create.'
  327. payload = 'name={} label={}'.format(new_name, label)
  328. if pool:
  329. payload += ' pool={}'.format(str(pool))
  330. method_prefix = 'admin.vm.CreateInPool.'
  331. if pools:
  332. payload += ''.join(' pool:{}={}'.format(vol, str(pool))
  333. for vol, pool in sorted(pools.items()))
  334. method_prefix = 'admin.vm.CreateInPool.'
  335. self.qubesd_call('dom0', method_prefix + new_cls, template,
  336. payload.encode('utf-8'))
  337. self.domains.clear_cache()
  338. dst_vm = self.domains[new_name]
  339. try:
  340. assert isinstance(dst_vm, qubesadmin.vm.QubesVM)
  341. for prop in src_vm.property_list():
  342. # handled by admin.vm.Create call
  343. if prop in ('name', 'qid', 'template', 'label', 'uuid',
  344. 'installed_by_rpm'):
  345. continue
  346. if src_vm.property_is_default(prop):
  347. continue
  348. try:
  349. setattr(dst_vm, prop, getattr(src_vm, prop))
  350. except AttributeError:
  351. pass
  352. except qubesadmin.exc.QubesException as e:
  353. dst_vm.log.error(
  354. 'Failed to set {!s} property: {!s}'.format(prop, e))
  355. if not ignore_errors:
  356. raise
  357. for tag in src_vm.tags:
  358. if tag.startswith('created-by-'):
  359. continue
  360. try:
  361. dst_vm.tags.add(tag)
  362. except qubesadmin.exc.QubesException as e:
  363. dst_vm.log.error(
  364. 'Failed to add {!s} tag: {!s}'.format(tag, e))
  365. if not ignore_errors:
  366. raise
  367. for feature, value in src_vm.features.items():
  368. try:
  369. dst_vm.features[feature] = value
  370. except qubesadmin.exc.QubesException as e:
  371. dst_vm.log.error(
  372. 'Failed to set {!s} feature: {!s}'.format(feature, e))
  373. if not ignore_errors:
  374. raise
  375. try:
  376. dst_vm.firewall.save_rules(src_vm.firewall.rules)
  377. except qubesadmin.exc.QubesException as e:
  378. self.log.error('Failed to set firewall: %s', e)
  379. if not ignore_errors:
  380. raise
  381. try:
  382. # FIXME: convert to qrexec calls to dom0/GUI VM
  383. appmenus_cmd = \
  384. ['qvm-appmenus', '--init', '--update',
  385. '--source', src_vm.name, dst_vm.name]
  386. subprocess.check_output(appmenus_cmd, stderr=subprocess.STDOUT)
  387. except OSError:
  388. # this file needs to be python 2.7 compatible,
  389. # so no FileNotFoundError
  390. self.log.error('Failed to clone appmenus, qvm-appmenus missing')
  391. if not ignore_errors:
  392. raise qubesadmin.exc.QubesException(
  393. 'Failed to clone appmenus')
  394. except subprocess.CalledProcessError as e:
  395. self.log.error('Failed to clone appmenus: %s',
  396. e.output.decode())
  397. if not ignore_errors:
  398. raise qubesadmin.exc.QubesException(
  399. 'Failed to clone appmenus')
  400. except qubesadmin.exc.QubesException:
  401. if not ignore_errors:
  402. del self.domains[dst_vm.name]
  403. raise
  404. try:
  405. for dst_volume in sorted(dst_vm.volumes.values()):
  406. if not dst_volume.save_on_stop:
  407. # clone only persistent volumes
  408. continue
  409. if ignore_volumes and dst_volume.name in ignore_volumes:
  410. continue
  411. src_volume = src_vm.volumes[dst_volume.name]
  412. dst_vm.log.info('Cloning {} volume'.format(dst_volume.name))
  413. dst_volume.clone(src_volume)
  414. except qubesadmin.exc.QubesException:
  415. del self.domains[dst_vm.name]
  416. raise
  417. if not ignore_devices:
  418. try:
  419. for devclass in src_vm.devices:
  420. for assignment in src_vm.devices[devclass].assignments(
  421. persistent=True):
  422. new_assignment = qubesadmin.devices.DeviceAssignment(
  423. backend_domain=assignment.backend_domain,
  424. ident=assignment.ident,
  425. options=assignment.options,
  426. persistent=assignment.persistent)
  427. dst_vm.devices[devclass].attach(new_assignment)
  428. except qubesadmin.exc.QubesException:
  429. if not ignore_errors:
  430. del self.domains[dst_vm.name]
  431. raise
  432. return dst_vm
  433. def qubesd_call(self, dest, method, arg=None, payload=None,
  434. payload_stream=None):
  435. """
  436. Execute Admin API method.
  437. If `payload` and `payload_stream` are both specified, they will be sent
  438. in that order.
  439. :param dest: Destination VM name
  440. :param method: Full API method name ('admin...')
  441. :param arg: Method argument (if any)
  442. :param payload: Payload send to the method
  443. :param payload_stream: file-like object to read payload from
  444. :return: Data returned by qubesd (string)
  445. .. warning:: *payload_stream* will get closed by this function
  446. """
  447. raise NotImplementedError(
  448. 'qubesd_call not implemented in QubesBase class; use specialized '
  449. 'class: qubesadmin.Qubes()')
  450. def run_service(self, dest, service, filter_esc=False, user=None,
  451. localcmd=None, wait=True, autostart=True, **kwargs):
  452. """Run qrexec service in a given destination
  453. *kwargs* are passed verbatim to :py:meth:`subprocess.Popen`.
  454. :param str dest: Destination - may be a VM name or empty
  455. string for default (for a given service)
  456. :param str service: service name
  457. :param bool filter_esc: filter escape sequences to protect terminal \
  458. emulator
  459. :param str user: username to run service as
  460. :param str localcmd: Command to connect stdin/stdout to
  461. :param bool wait: Wait service run
  462. :param bool autostart: Automatically start the target VM
  463. :rtype: subprocess.Popen
  464. """
  465. raise NotImplementedError(
  466. 'run_service not implemented in QubesBase class; use specialized '
  467. 'class: qubesadmin.Qubes()')
  468. @staticmethod
  469. def _call_with_stream(command, payload, payload_stream):
  470. """Helper method to pass data to qubesd. Calls a command with
  471. payload and payload_stream as input.
  472. :param command: command to run
  473. :param payload: Initial payload, or None
  474. :param payload_stream: File-like object with the rest of data
  475. :return: (process, stdout, stderr)
  476. """
  477. if payload:
  478. # It's not strictly correct to write data to stdin in this way,
  479. # because the process can get blocked on stdout or stderr pipe.
  480. # However, in practice the output should be always smaller than 4K.
  481. proc = subprocess.Popen(
  482. command,
  483. stdin=subprocess.PIPE,
  484. stdout=subprocess.PIPE,
  485. stderr=subprocess.PIPE)
  486. proc.stdin.write(payload)
  487. try:
  488. shutil.copyfileobj(payload_stream, proc.stdin)
  489. except BrokenPipeError:
  490. # We might receive an error from qubesd before we sent
  491. # everything (for instance, because we are sending too much
  492. # data).
  493. pass
  494. else:
  495. # Connect the stream directly.
  496. proc = subprocess.Popen(
  497. command,
  498. stdin=payload_stream,
  499. stdout=subprocess.PIPE,
  500. stderr=subprocess.PIPE)
  501. payload_stream.close()
  502. stdout, stderr = proc.communicate()
  503. return proc, stdout, stderr
  504. def _invalidate_cache(self, subject, event, name, **kwargs):
  505. """Invalidate cached value of a property.
  506. This method is designed to be hooked as an event handler for:
  507. - property-set:*
  508. - property-del:*
  509. This is done in :py:class:`qubesadmin.events.EventsDispatcher` class
  510. directly, before calling other handlers.
  511. It handles both VM and global properties.
  512. Note: even if the new value is given in the event arguments, it is
  513. ignored because it comes without type information.
  514. :param subject: either VM object or None
  515. :param event: name of the event
  516. :param name: name of the property
  517. :param kwargs: other arguments
  518. :return: none
  519. """ # pylint: disable=unused-argument
  520. if subject is None:
  521. subject = self
  522. try:
  523. # pylint: disable=protected-access
  524. del subject._properties_cache[name]
  525. except KeyError:
  526. pass
  527. def _update_power_state_cache(self, subject, event, **kwargs):
  528. """ Update cached VM power state.
  529. This method is designed to be hooed as an event handler for:
  530. - domain-pre-start
  531. - domain-start
  532. - domain-shutdown
  533. - domain-paused
  534. - domain-unpaused
  535. This is done in :py:class:`qubesadmin.events.EventsDispatcher` class
  536. directly, before calling other handlers.
  537. :param subject: a VM object
  538. :param event: name of the event
  539. :param kwargs: other arguments
  540. :return:
  541. """ # pylint: disable=unused-argument,no-self-use
  542. if not self.app.cache_enabled:
  543. return
  544. if event == 'domain-pre-start':
  545. power_state = 'Transient'
  546. elif event == 'domain-start':
  547. power_state = 'Running'
  548. elif event == 'domain-shutdown':
  549. power_state = 'Halted'
  550. elif event == 'domain-paused':
  551. power_state = 'Paused'
  552. elif event == 'domain-unpaused':
  553. power_state = 'Running'
  554. else:
  555. # unknown power state change, drop cached power state
  556. power_state = None
  557. # pylint: disable=protected-access
  558. subject._power_state_cache = power_state
  559. class QubesLocal(QubesBase):
  560. """Application object communicating through local socket.
  561. Used when running in dom0.
  562. """
  563. qubesd_connection_type = 'socket'
  564. def qubesd_call(self, dest, method, arg=None, payload=None,
  565. payload_stream=None):
  566. """
  567. Execute Admin API method.
  568. If `payload` and `payload_stream` are both specified, they will be sent
  569. in that order.
  570. :param dest: Destination VM name
  571. :param method: Full API method name ('admin...')
  572. :param arg: Method argument (if any)
  573. :param payload: Payload send to the method
  574. :param payload_stream: file-like object to read payload from
  575. :return: Data returned by qubesd (string)
  576. .. warning:: *payload_stream* will get closed by this function
  577. """
  578. if payload_stream:
  579. # payload_stream can be used for large amount of data,
  580. # so optimize for throughput, not latency: spawn actual qrexec
  581. # service implementation, which may use some optimization there (
  582. # see admin.vm.volume.Import - actual data handling is done with dd)
  583. method_path = os.path.join(
  584. qubesadmin.config.QREXEC_SERVICES_DIR, method)
  585. if not os.path.exists(method_path):
  586. raise qubesadmin.exc.QubesDaemonCommunicationError(
  587. '{} not found'.format(method_path))
  588. command = ['env', 'QREXEC_REMOTE_DOMAIN=dom0',
  589. 'QREXEC_REQUESTED_TARGET=' + dest, method_path, arg]
  590. if os.getuid() != 0:
  591. command.insert(0, 'sudo')
  592. (_, stdout, _) = self._call_with_stream(
  593. command, payload, payload_stream)
  594. return self._parse_qubesd_response(stdout)
  595. try:
  596. client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  597. client_socket.connect(qubesadmin.config.QUBESD_SOCKET)
  598. except (IOError, OSError) as e:
  599. raise qubesadmin.exc.QubesDaemonCommunicationError(
  600. 'Failed to connect to qubesd service: %s', str(e))
  601. call_header = '{}+{} dom0 name {}\0'.format(method, arg or '', dest)
  602. client_socket.sendall(call_header.encode('ascii'))
  603. if payload is not None:
  604. client_socket.sendall(payload)
  605. client_socket.shutdown(socket.SHUT_WR)
  606. return_data = client_socket.makefile('rb').read()
  607. client_socket.close()
  608. return self._parse_qubesd_response(return_data)
  609. def run_service(self, dest, service, filter_esc=False, user=None,
  610. localcmd=None, wait=True, autostart=True, **kwargs):
  611. """Run qrexec service in a given destination
  612. :param str dest: Destination - may be a VM name or empty
  613. string for default (for a given service)
  614. :param str service: service name
  615. :param bool filter_esc: filter escape sequences to protect terminal \
  616. emulator
  617. :param str user: username to run service as
  618. :param str localcmd: Command to connect stdin/stdout to
  619. :param bool wait: wait for remote process to finish
  620. :rtype: subprocess.Popen
  621. """
  622. if not dest:
  623. raise ValueError('Empty destination name allowed only from a VM')
  624. if not wait and localcmd:
  625. raise ValueError('wait=False incompatible with localcmd')
  626. if autostart:
  627. try:
  628. self.qubesd_call(dest, 'admin.vm.Start')
  629. except qubesadmin.exc.QubesVMNotHaltedError:
  630. pass
  631. elif not self.domains.get_blind(dest).is_running():
  632. raise qubesadmin.exc.QubesVMNotRunningError(
  633. '%s is not running', dest)
  634. if dest == 'dom0':
  635. # can't make real dom0->dom0 call
  636. if filter_esc:
  637. raise NotImplementedError(
  638. 'filter_esc=True not implemented in dom0->dom0 calls')
  639. if localcmd:
  640. raise NotImplementedError(
  641. 'localcmd not implemented in dom0->dom0 calls')
  642. if not wait:
  643. raise NotImplementedError(
  644. 'wait=False not implemented in dom0->dom0 calls')
  645. if user is None:
  646. user = grp.getgrnam('qubes').gr_mem[0]
  647. kwargs.setdefault('stdin', subprocess.PIPE)
  648. kwargs.setdefault('stdout', subprocess.PIPE)
  649. kwargs.setdefault('stderr', subprocess.PIPE)
  650. # Set default locale to C in order to prevent error msg
  651. # in subprocess call related to falling back to C locale
  652. env = os.environ.copy()
  653. env['LC_ALL'] = 'C'
  654. cmd = '/etc/qubes-rpc/' + service
  655. arg = ''
  656. if not os.path.exists(cmd) and '+' in service:
  657. cmd, arg = cmd.split('+', 1)
  658. p = subprocess.Popen(
  659. ['sudo', '-u', user, cmd, arg],
  660. **kwargs,
  661. env=env,
  662. )
  663. return p
  664. qrexec_opts = ['-d', dest]
  665. if filter_esc:
  666. qrexec_opts.extend(['-t'])
  667. if filter_esc or os.isatty(sys.stderr.fileno()):
  668. qrexec_opts.extend(['-T'])
  669. if localcmd:
  670. qrexec_opts.extend(['-l', localcmd])
  671. if user is None:
  672. user = 'DEFAULT'
  673. if not wait:
  674. qrexec_opts.extend(['-e'])
  675. if 'connect_timeout' in kwargs:
  676. qrexec_opts.extend(['-w', str(kwargs.pop('connect_timeout'))])
  677. kwargs.setdefault('stdin', subprocess.PIPE)
  678. kwargs.setdefault('stdout', subprocess.PIPE)
  679. kwargs.setdefault('stderr', subprocess.PIPE)
  680. proc = subprocess.Popen(
  681. [qubesadmin.config.QREXEC_CLIENT] + qrexec_opts + [
  682. '{}:QUBESRPC {} dom0'.format(user, service)], **kwargs)
  683. return proc
  684. class QubesRemote(QubesBase):
  685. """Application object communicating through qrexec services.
  686. Used when running in VM.
  687. """
  688. qubesd_connection_type = 'qrexec'
  689. def qubesd_call(self, dest, method, arg=None, payload=None,
  690. payload_stream=None):
  691. """
  692. Execute Admin API method.
  693. If `payload` and `payload_stream` are both specified, they will be sent
  694. in that order.
  695. :param dest: Destination VM name
  696. :param method: Full API method name ('admin...')
  697. :param arg: Method argument (if any)
  698. :param payload: Payload send to the method
  699. :param payload_stream: file-like object to read payload from
  700. :return: Data returned by qubesd (string)
  701. .. warning:: *payload_stream* will get closed by this function
  702. """
  703. service_name = method
  704. if arg is not None:
  705. service_name += '+' + arg
  706. command = [qubesadmin.config.QREXEC_CLIENT_VM,
  707. dest, service_name]
  708. if payload_stream:
  709. (p, stdout, stderr) = self._call_with_stream(
  710. command, payload, payload_stream)
  711. else:
  712. p = subprocess.Popen(command,
  713. stdin=subprocess.PIPE,
  714. stdout=subprocess.PIPE,
  715. stderr=subprocess.PIPE)
  716. (stdout, stderr) = p.communicate(payload)
  717. if p.returncode != 0:
  718. raise qubesadmin.exc.QubesDaemonNoResponseError(
  719. 'Service call error: %s', stderr.decode())
  720. return self._parse_qubesd_response(stdout)
  721. def run_service(self, dest, service, filter_esc=False, user=None,
  722. localcmd=None, wait=True, autostart=True, **kwargs):
  723. """Run qrexec service in a given destination
  724. :param str dest: Destination - may be a VM name or empty
  725. string for default (for a given service)
  726. :param str service: service name
  727. :param bool filter_esc: filter escape sequences to protect terminal \
  728. emulator
  729. :param str user: username to run service as
  730. :param str localcmd: Command to connect stdin/stdout to
  731. :param bool wait: wait for process to finish
  732. :rtype: subprocess.Popen
  733. """
  734. if not autostart and not dest:
  735. raise ValueError(
  736. 'autostart=False makes sense only with a defined target')
  737. if user:
  738. raise ValueError(
  739. 'non-default user not possible for calls from VM')
  740. if not wait and localcmd:
  741. raise ValueError('wait=False incompatible with localcmd')
  742. qrexec_opts = []
  743. if filter_esc:
  744. qrexec_opts.extend(['-t'])
  745. if filter_esc or (
  746. os.isatty(sys.stderr.fileno()) and 'stderr' not in kwargs):
  747. qrexec_opts.extend(['-T'])
  748. if not autostart and not self.domains.get_blind(dest).is_running():
  749. raise qubesadmin.exc.QubesVMNotRunningError(
  750. '%s is not running', dest)
  751. if not wait:
  752. # qrexec-client-vm can only request service calls, which are
  753. # started using MSG_EXEC_CMDLINE qrexec protocol message; this
  754. # message means "start the process, pipe its stdin/out/err,
  755. # and when it terminates, send exit code back".
  756. # According to the protocol qrexec-client-vm needs to wait for
  757. # MSG_DATA_EXIT_CODE, so implementing wait=False would require
  758. # some protocol change (or protocol violation).
  759. raise NotImplementedError(
  760. 'wait=False not implemented for calls from VM')
  761. kwargs.setdefault('stdin', subprocess.PIPE)
  762. kwargs.setdefault('stdout', subprocess.PIPE)
  763. kwargs.setdefault('stderr', subprocess.PIPE)
  764. proc = subprocess.Popen(
  765. [qubesadmin.config.QREXEC_CLIENT_VM] +
  766. qrexec_opts +
  767. [dest or '', service] +
  768. (shlex.split(localcmd) if localcmd else []),
  769. **kwargs)
  770. return proc