events.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. import socket
  21. import subprocess
  22. import qubesadmin.tests
  23. import unittest
  24. try:
  25. # qubesadmin.events require python3, so this tests can also use python3 features
  26. import asyncio
  27. import unittest.mock
  28. import qubesadmin.events
  29. except ImportError:
  30. # don't run any tests on python2
  31. def load_tests(loader, tests, pattern):
  32. return unittest.TestSuite()
  33. # don't fail on coroutine decorator
  34. class asyncio(object):
  35. @staticmethod
  36. def coroutine(f):
  37. return f
  38. class TC_00_Events(qubesadmin.tests.QubesTestCase):
  39. def setUp(self):
  40. super().setUp()
  41. self.app = unittest.mock.MagicMock()
  42. self.dispatcher = qubesadmin.events.EventsDispatcher(self.app)
  43. def test_000_handler_specific(self):
  44. handler = unittest.mock.Mock()
  45. self.dispatcher.add_handler('some-event', handler)
  46. self.dispatcher.handle('', 'some-event', arg1='value1')
  47. handler.assert_called_once_with(None, 'some-event', arg1='value1')
  48. handler.reset_mock()
  49. self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
  50. handler.assert_called_once_with(
  51. self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
  52. handler.reset_mock()
  53. self.dispatcher.handle('', 'other-event', arg1='value1')
  54. self.assertFalse(handler.called)
  55. self.dispatcher.remove_handler('some-event', handler)
  56. self.dispatcher.handle('', 'some-event', arg1='value1')
  57. self.assertFalse(handler.called)
  58. def test_001_handler_glob(self):
  59. handler = unittest.mock.Mock()
  60. self.dispatcher.add_handler('*', handler)
  61. self.dispatcher.handle('', 'some-event', arg1='value1')
  62. handler.assert_called_once_with(None, 'some-event', arg1='value1')
  63. handler.reset_mock()
  64. self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
  65. handler.assert_called_once_with(
  66. self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
  67. handler.reset_mock()
  68. self.dispatcher.handle('', 'other-event', arg1='value1')
  69. handler.assert_called_once_with(None, 'other-event', arg1='value1')
  70. handler.reset_mock()
  71. self.dispatcher.remove_handler('*', handler)
  72. self.dispatcher.handle('', 'some-event', arg1='value1')
  73. self.assertFalse(handler.called)
  74. def test_002_handler_glob_partial(self):
  75. handler = unittest.mock.Mock()
  76. self.dispatcher.add_handler('some-*', handler)
  77. self.dispatcher.handle('', 'some-event', arg1='value1')
  78. handler.assert_called_once_with(None, 'some-event', arg1='value1')
  79. handler.reset_mock()
  80. self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
  81. handler.assert_called_once_with(
  82. self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
  83. handler.reset_mock()
  84. self.dispatcher.handle('', 'other-event', arg1='value1')
  85. self.assertFalse(handler.called)
  86. handler.reset_mock()
  87. self.dispatcher.remove_handler('some-*', handler)
  88. self.dispatcher.handle('', 'some-event', arg1='value1')
  89. self.assertFalse(handler.called)
  90. @asyncio.coroutine
  91. def mock_get_events_reader(self, stream, cleanup_func, expected_vm,
  92. vm=None):
  93. self.assertEqual(expected_vm, vm)
  94. return stream, cleanup_func
  95. @asyncio.coroutine
  96. def send_events(self, stream, events):
  97. for event in events:
  98. stream.feed_data(event)
  99. # don't use yield from...
  100. for x in asyncio.sleep(0.01):
  101. yield x
  102. stream.feed_eof()
  103. def test_010_listen_for_events(self):
  104. loop = asyncio.new_event_loop()
  105. asyncio.set_event_loop(loop)
  106. stream = asyncio.StreamReader()
  107. cleanup_func = unittest.mock.Mock()
  108. self.dispatcher._get_events_reader = \
  109. lambda vm: self.mock_get_events_reader(stream, cleanup_func,
  110. None, vm)
  111. handler = unittest.mock.Mock()
  112. self.dispatcher.add_handler('some-event', handler)
  113. events = [
  114. b'1\0\0some-event\0arg1\0value1\0\0',
  115. b'1\0some-vm\0some-event\0arg1\0value1\0\0',
  116. b'1\0some-vm\0some-event\0arg_without_value\0\0arg2\0value\0\0',
  117. b'1\0some-vm\0other-event\0\0',
  118. ]
  119. asyncio.ensure_future(self.send_events(stream, events))
  120. loop.run_until_complete(self.dispatcher.listen_for_events(
  121. reconnect=False))
  122. self.assertEqual(handler.mock_calls, [
  123. unittest.mock.call(None, 'some-event', arg1='value1'),
  124. unittest.mock.call(
  125. self.app.domains.get_blind('some-vm'), 'some-event',
  126. arg1='value1'),
  127. unittest.mock.call(
  128. self.app.domains.get_blind('some-vm'), 'some-event',
  129. arg_without_value='', arg2='value'),
  130. ])
  131. cleanup_func.assert_called_once_with()
  132. loop.close()
  133. def mock_open_unix_connection(self, expected_path, sock, path):
  134. self.assertEqual(expected_path, path)
  135. return asyncio.open_connection(sock=sock)
  136. def read_all(self, sock):
  137. buf = b''
  138. for data in iter(lambda: sock.recv(4096), b''):
  139. buf += data
  140. return buf
  141. def test_020_get_events_reader_local(self):
  142. self.app.qubesd_connection_type = 'socket'
  143. loop = asyncio.new_event_loop()
  144. asyncio.set_event_loop(loop)
  145. sock1, sock2 = socket.socketpair()
  146. with unittest.mock.patch('asyncio.open_unix_connection',
  147. lambda path: self.mock_open_unix_connection(
  148. qubesadmin.config.QUBESD_SOCKET, sock1, path)):
  149. task = asyncio.ensure_future(self.dispatcher._get_events_reader())
  150. reader = asyncio.ensure_future(loop.run_in_executor(None,
  151. self.read_all, sock2))
  152. loop.run_until_complete(asyncio.wait([task, reader]))
  153. self.assertEqual(reader.result(),
  154. b'dom0\0admin.Events\0dom0\0\0')
  155. self.assertIsInstance(task.result()[0], asyncio.StreamReader)
  156. cleanup_func = task.result()[1]
  157. cleanup_func()
  158. sock2.close()
  159. # run socket cleanup functions
  160. loop.stop()
  161. loop.run_forever()
  162. loop.close()
  163. def test_021_get_events_reader_local_vm(self):
  164. self.app.qubesd_connection_type = 'socket'
  165. loop = asyncio.new_event_loop()
  166. asyncio.set_event_loop(loop)
  167. sock1, sock2 = socket.socketpair()
  168. vm = unittest.mock.Mock()
  169. vm.name = 'test-vm'
  170. with unittest.mock.patch('asyncio.open_unix_connection',
  171. lambda path: self.mock_open_unix_connection(
  172. qubesadmin.config.QUBESD_SOCKET, sock1, path)):
  173. task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
  174. reader = asyncio.ensure_future(loop.run_in_executor(None,
  175. self.read_all, sock2))
  176. loop.run_until_complete(asyncio.wait([task, reader]))
  177. self.assertEqual(reader.result(),
  178. b'dom0\0admin.Events\0test-vm\0\0')
  179. self.assertIsInstance(task.result()[0], asyncio.StreamReader)
  180. cleanup_func = task.result()[1]
  181. cleanup_func()
  182. sock2.close()
  183. # run socket cleanup functions
  184. loop.stop()
  185. loop.run_forever()
  186. loop.close()
  187. @asyncio.coroutine
  188. def mock_coroutine(self, mock, *args, **kwargs):
  189. return mock(*args, **kwargs)
  190. def test_022_get_events_reader_remote(self):
  191. self.app.qubesd_connection_type = 'qrexec'
  192. loop = asyncio.new_event_loop()
  193. asyncio.set_event_loop(loop)
  194. mock_proc = unittest.mock.Mock()
  195. with unittest.mock.patch('asyncio.create_subprocess_exec',
  196. lambda *args, **kwargs: self.mock_coroutine(mock_proc,
  197. *args, **kwargs)):
  198. task = asyncio.ensure_future(self.dispatcher._get_events_reader())
  199. loop.run_until_complete(task)
  200. self.assertEqual(mock_proc.mock_calls, [
  201. unittest.mock.call('qrexec-client-vm', 'dom0',
  202. 'admin.Events', stdin=subprocess.PIPE,
  203. stdout=subprocess.PIPE),
  204. unittest.mock.call().stdin.write_eof()
  205. ])
  206. self.assertEqual(task.result()[0], mock_proc().stdout)
  207. cleanup_func = task.result()[1]
  208. cleanup_func()
  209. unittest.mock.call().kill.assert_called_once_with()
  210. loop.close()
  211. def test_023_get_events_reader_remote_vm(self):
  212. self.app.qubesd_connection_type = 'qrexec'
  213. loop = asyncio.new_event_loop()
  214. asyncio.set_event_loop(loop)
  215. mock_proc = unittest.mock.Mock()
  216. vm = unittest.mock.Mock()
  217. vm.name = 'test-vm'
  218. with unittest.mock.patch('asyncio.create_subprocess_exec',
  219. lambda *args, **kwargs: self.mock_coroutine(mock_proc,
  220. *args, **kwargs)):
  221. task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
  222. loop.run_until_complete(task)
  223. self.assertEqual(mock_proc.mock_calls, [
  224. unittest.mock.call('qrexec-client-vm', 'test-vm',
  225. 'admin.Events', stdin=subprocess.PIPE,
  226. stdout=subprocess.PIPE),
  227. unittest.mock.call().stdin.write_eof()
  228. ])
  229. self.assertEqual(task.result()[0], mock_proc().stdout)
  230. cleanup_func = task.result()[1]
  231. cleanup_func()
  232. unittest.mock.call().kill.assert_called_once_with()
  233. loop.close()
  234. def test_030_events_device(self):
  235. handler = unittest.mock.Mock()
  236. self.dispatcher.add_handler('device-attach:test', handler)
  237. self.dispatcher.handle('test-vm', 'device-attach:test',
  238. device='test-vm2:dev', options='{}')
  239. vm = self.app.domains.get_blind('test-vm')
  240. dev = self.app.domains.get_blind('test-vm2').devices['test']['dev']
  241. handler.assert_called_once_with(vm, 'device-attach:test', device=dev,
  242. options='{}')