internal.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library 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 GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. ''' Internal interface for dom0 components to communicate with qubesd. '''
  21. import asyncio
  22. import json
  23. import subprocess
  24. import qubes.api
  25. import qubes.api.admin
  26. import qubes.vm.adminvm
  27. import qubes.vm.dispvm
  28. def get_system_info(app):
  29. system_info = {'domains': {
  30. domain.name: {
  31. 'tags': list(domain.tags),
  32. 'type': domain.__class__.__name__,
  33. 'template_for_dispvms':
  34. getattr(domain, 'template_for_dispvms', False),
  35. 'default_dispvm': (domain.default_dispvm.name if
  36. getattr(domain, 'default_dispvm', None) else None),
  37. 'icon': str(domain.label.icon),
  38. 'guivm': (domain.guivm.name if getattr(domain, 'guivm', None)
  39. else None),
  40. 'power_state': domain.get_power_state(),
  41. } for domain in app.domains
  42. }}
  43. return system_info
  44. class QubesInternalAPI(qubes.api.AbstractQubesAPI):
  45. ''' Communication interface for dom0 components,
  46. by design the input here is trusted.'''
  47. SOCKNAME = '/var/run/qubesd.internal.sock'
  48. @qubes.api.method('internal.GetSystemInfo', no_payload=True)
  49. @asyncio.coroutine
  50. def getsysteminfo(self):
  51. self.enforce(self.dest.name == 'dom0')
  52. self.enforce(not self.arg)
  53. system_info = get_system_info(self.app)
  54. return json.dumps(system_info)
  55. @qubes.api.method('internal.vm.volume.ImportBegin',
  56. scope='local', write=True)
  57. @asyncio.coroutine
  58. def vm_volume_import(self, untrusted_payload):
  59. """Begin importing volume data. Payload is either size of new data
  60. in bytes, or empty. If empty, the current volume's size will be used.
  61. Returns size and path to where data should be written.
  62. Triggered by scripts in /etc/qubes-rpc:
  63. admin.vm.volume.Import, admin.vm.volume.ImportWithSize.
  64. When the script finish importing, it will trigger
  65. internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
  66. payload) and response from that call will be actually send to the
  67. caller.
  68. """
  69. self.enforce(self.arg in self.dest.volumes.keys())
  70. if untrusted_payload:
  71. original_method = 'admin.vm.volume.ImportWithSize'
  72. else:
  73. original_method = 'admin.vm.volume.Import'
  74. self.src.fire_event(
  75. 'admin-permission:' + original_method,
  76. pre_event=True, dest=self.dest, arg=self.arg)
  77. if not self.dest.is_halted():
  78. raise qubes.exc.QubesVMNotHaltedError(self.dest)
  79. requested_size = None
  80. if untrusted_payload:
  81. try:
  82. untrusted_value = int(untrusted_payload.decode('ascii'))
  83. except (UnicodeDecodeError, ValueError):
  84. raise qubes.api.ProtocolError('Invalid value')
  85. self.enforce(untrusted_value > 0)
  86. requested_size = untrusted_value
  87. del untrusted_value
  88. del untrusted_payload
  89. path = yield from self.dest.storage.import_data(
  90. self.arg, requested_size)
  91. self.enforce(' ' not in path)
  92. if requested_size is None:
  93. size = self.dest.volumes[self.arg].size
  94. else:
  95. size = requested_size
  96. # when we know the action is allowed, inform extensions that it will
  97. # be performed
  98. self.dest.fire_event(
  99. 'domain-volume-import-begin', volume=self.arg, size=size)
  100. return '{} {}'.format(size, path)
  101. @qubes.api.method('internal.vm.volume.ImportEnd')
  102. @asyncio.coroutine
  103. def vm_volume_import_end(self, untrusted_payload):
  104. '''
  105. This is second half of admin.vm.volume.Import handling. It is called
  106. when actual import is finished. Response from this method is sent do
  107. the client (as a response for admin.vm.volume.Import call).
  108. The payload is either 'ok', or 'fail\n<error message>'.
  109. '''
  110. self.enforce(self.arg in self.dest.volumes.keys())
  111. success = untrusted_payload == b'ok'
  112. try:
  113. yield from self.dest.storage.import_data_end(self.arg,
  114. success=success)
  115. except:
  116. self.dest.fire_event('domain-volume-import-end', volume=self.arg,
  117. success=False)
  118. raise
  119. self.dest.fire_event('domain-volume-import-end', volume=self.arg,
  120. success=success)
  121. if not success:
  122. error = ''
  123. parts = untrusted_payload.decode('ascii').split('\n', 1)
  124. if len(parts) > 1:
  125. error = parts[1]
  126. raise qubes.exc.QubesException(
  127. 'Data import failed: {}'.format(error))
  128. @qubes.api.method('internal.SuspendPre', no_payload=True)
  129. @asyncio.coroutine
  130. def suspend_pre(self):
  131. '''
  132. Method called before host system goes to sleep.
  133. :return:
  134. '''
  135. # first notify all VMs
  136. processes = []
  137. for vm in self.app.domains:
  138. if isinstance(vm, qubes.vm.adminvm.AdminVM):
  139. continue
  140. if not vm.is_running():
  141. continue
  142. if not vm.features.check_with_template('qrexec', False):
  143. continue
  144. try:
  145. proc = yield from vm.run_service(
  146. 'qubes.SuspendPreAll', user='root',
  147. stdin=subprocess.DEVNULL,
  148. stdout=subprocess.DEVNULL,
  149. stderr=subprocess.DEVNULL)
  150. processes.append(proc)
  151. except qubes.exc.QubesException as e:
  152. vm.log.warning('Failed to run qubes.SuspendPreAll: %s', str(e))
  153. # FIXME: some timeout?
  154. if processes:
  155. yield from asyncio.wait([p.wait() for p in processes])
  156. coros = []
  157. # then suspend/pause VMs
  158. for vm in self.app.domains:
  159. if isinstance(vm, qubes.vm.adminvm.AdminVM):
  160. continue
  161. if vm.is_running():
  162. coros.append(vm.suspend())
  163. if coros:
  164. yield from asyncio.wait(coros)
  165. @qubes.api.method('internal.SuspendPost', no_payload=True)
  166. @asyncio.coroutine
  167. def suspend_post(self):
  168. '''
  169. Method called after host system wake up from sleep.
  170. :return:
  171. '''
  172. coros = []
  173. # first resume/unpause VMs
  174. for vm in self.app.domains:
  175. if isinstance(vm, qubes.vm.adminvm.AdminVM):
  176. continue
  177. if vm.get_power_state() in ["Paused", "Suspended"]:
  178. coros.append(vm.resume())
  179. if coros:
  180. yield from asyncio.wait(coros)
  181. # then notify all VMs
  182. processes = []
  183. for vm in self.app.domains:
  184. if isinstance(vm, qubes.vm.adminvm.AdminVM):
  185. continue
  186. if not vm.is_running():
  187. continue
  188. if not vm.features.check_with_template('qrexec', False):
  189. continue
  190. try:
  191. proc = yield from vm.run_service(
  192. 'qubes.SuspendPostAll', user='root',
  193. stdin=subprocess.DEVNULL,
  194. stdout=subprocess.DEVNULL,
  195. stderr=subprocess.DEVNULL)
  196. processes.append(proc)
  197. except qubes.exc.QubesException as e:
  198. vm.log.warning('Failed to run qubes.SuspendPostAll: %s', str(e))
  199. # FIXME: some timeout?
  200. if processes:
  201. yield from asyncio.wait([p.wait() for p in processes])