2017-04-14 22:02:43 +02:00
|
|
|
# -*- encoding: utf8 -*-
|
|
|
|
#
|
|
|
|
# The Qubes OS Project, http://www.qubes-os.org
|
|
|
|
#
|
|
|
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
|
|
|
# <marmarek@invisiblethingslab.com>
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Lesser General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
|
|
|
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import socket
|
|
|
|
import subprocess
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin.tests
|
2017-04-14 22:02:43 +02:00
|
|
|
import unittest
|
|
|
|
try:
|
2017-05-11 23:21:04 +02:00
|
|
|
# qubesadmin.events require python3, so this tests can also use python3 features
|
2017-04-14 22:02:43 +02:00
|
|
|
import asyncio
|
|
|
|
import unittest.mock
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin.events
|
2017-04-14 22:02:43 +02:00
|
|
|
except ImportError:
|
|
|
|
# don't run any tests on python2
|
|
|
|
def load_tests(loader, tests, pattern):
|
|
|
|
return unittest.TestSuite()
|
|
|
|
# don't fail on coroutine decorator
|
|
|
|
class asyncio(object):
|
|
|
|
@staticmethod
|
|
|
|
def coroutine(f):
|
|
|
|
return f
|
|
|
|
|
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
class TC_00_Events(qubesadmin.tests.QubesTestCase):
|
2017-04-14 22:02:43 +02:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
|
|
|
self.app = unittest.mock.MagicMock()
|
2017-05-11 23:21:04 +02:00
|
|
|
self.dispatcher = qubesadmin.events.EventsDispatcher(self.app)
|
2017-04-14 22:02:43 +02:00
|
|
|
|
|
|
|
def test_000_handler_specific(self):
|
|
|
|
handler = unittest.mock.Mock()
|
|
|
|
self.dispatcher.add_handler('some-event', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(None, 'some-event', arg1='value1')
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(
|
2018-06-27 01:23:56 +02:00
|
|
|
self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
|
2017-04-14 22:02:43 +02:00
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('', 'other-event', arg1='value1')
|
|
|
|
self.assertFalse(handler.called)
|
|
|
|
self.dispatcher.remove_handler('some-event', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
self.assertFalse(handler.called)
|
|
|
|
|
|
|
|
def test_001_handler_glob(self):
|
|
|
|
handler = unittest.mock.Mock()
|
|
|
|
self.dispatcher.add_handler('*', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(None, 'some-event', arg1='value1')
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(
|
2018-06-27 01:23:56 +02:00
|
|
|
self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
|
2017-04-14 22:02:43 +02:00
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('', 'other-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(None, 'other-event', arg1='value1')
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.remove_handler('*', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
self.assertFalse(handler.called)
|
|
|
|
|
2018-09-07 18:59:05 +02:00
|
|
|
def test_002_handler_glob_partial(self):
|
|
|
|
handler = unittest.mock.Mock()
|
|
|
|
self.dispatcher.add_handler('some-*', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(None, 'some-event', arg1='value1')
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
|
|
|
|
handler.assert_called_once_with(
|
|
|
|
self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.handle('', 'other-event', arg1='value1')
|
|
|
|
self.assertFalse(handler.called)
|
|
|
|
handler.reset_mock()
|
|
|
|
self.dispatcher.remove_handler('some-*', handler)
|
|
|
|
self.dispatcher.handle('', 'some-event', arg1='value1')
|
|
|
|
self.assertFalse(handler.called)
|
|
|
|
|
2017-04-14 22:02:43 +02:00
|
|
|
@asyncio.coroutine
|
|
|
|
def mock_get_events_reader(self, stream, cleanup_func, expected_vm,
|
|
|
|
vm=None):
|
|
|
|
self.assertEqual(expected_vm, vm)
|
|
|
|
return stream, cleanup_func
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def send_events(self, stream, events):
|
|
|
|
for event in events:
|
|
|
|
stream.feed_data(event)
|
|
|
|
# don't use yield from...
|
|
|
|
for x in asyncio.sleep(0.01):
|
|
|
|
yield x
|
|
|
|
stream.feed_eof()
|
|
|
|
|
|
|
|
def test_010_listen_for_events(self):
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
stream = asyncio.StreamReader()
|
|
|
|
cleanup_func = unittest.mock.Mock()
|
|
|
|
self.dispatcher._get_events_reader = \
|
|
|
|
lambda vm: self.mock_get_events_reader(stream, cleanup_func,
|
|
|
|
None, vm)
|
|
|
|
handler = unittest.mock.Mock()
|
|
|
|
self.dispatcher.add_handler('some-event', handler)
|
|
|
|
events = [
|
|
|
|
b'1\0\0some-event\0arg1\0value1\0\0',
|
|
|
|
b'1\0some-vm\0some-event\0arg1\0value1\0\0',
|
2017-05-30 01:31:13 +02:00
|
|
|
b'1\0some-vm\0some-event\0arg_without_value\0\0arg2\0value\0\0',
|
2017-04-14 22:02:43 +02:00
|
|
|
b'1\0some-vm\0other-event\0\0',
|
|
|
|
]
|
|
|
|
asyncio.ensure_future(self.send_events(stream, events))
|
2017-04-15 20:12:42 +02:00
|
|
|
loop.run_until_complete(self.dispatcher.listen_for_events(
|
|
|
|
reconnect=False))
|
2017-04-14 22:02:43 +02:00
|
|
|
self.assertEqual(handler.mock_calls, [
|
|
|
|
unittest.mock.call(None, 'some-event', arg1='value1'),
|
|
|
|
unittest.mock.call(
|
2018-06-27 01:23:56 +02:00
|
|
|
self.app.domains.get_blind('some-vm'), 'some-event',
|
|
|
|
arg1='value1'),
|
2017-05-30 01:31:13 +02:00
|
|
|
unittest.mock.call(
|
2018-06-27 01:23:56 +02:00
|
|
|
self.app.domains.get_blind('some-vm'), 'some-event',
|
2017-05-30 01:31:13 +02:00
|
|
|
arg_without_value='', arg2='value'),
|
2017-04-14 22:02:43 +02:00
|
|
|
])
|
|
|
|
cleanup_func.assert_called_once_with()
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
def mock_open_unix_connection(self, expected_path, sock, path):
|
|
|
|
self.assertEqual(expected_path, path)
|
|
|
|
return asyncio.open_connection(sock=sock)
|
|
|
|
|
|
|
|
def read_all(self, sock):
|
|
|
|
buf = b''
|
|
|
|
for data in iter(lambda: sock.recv(4096), b''):
|
|
|
|
buf += data
|
|
|
|
return buf
|
|
|
|
|
|
|
|
def test_020_get_events_reader_local(self):
|
|
|
|
self.app.qubesd_connection_type = 'socket'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
sock1, sock2 = socket.socketpair()
|
|
|
|
with unittest.mock.patch('asyncio.open_unix_connection',
|
|
|
|
lambda path: self.mock_open_unix_connection(
|
2017-05-11 23:21:04 +02:00
|
|
|
qubesadmin.config.QUBESD_SOCKET, sock1, path)):
|
2017-04-14 22:02:43 +02:00
|
|
|
task = asyncio.ensure_future(self.dispatcher._get_events_reader())
|
|
|
|
reader = asyncio.ensure_future(loop.run_in_executor(None,
|
|
|
|
self.read_all, sock2))
|
|
|
|
loop.run_until_complete(asyncio.wait([task, reader]))
|
|
|
|
self.assertEqual(reader.result(),
|
2017-05-12 19:36:03 +02:00
|
|
|
b'dom0\0admin.Events\0dom0\0\0')
|
2017-04-14 22:02:43 +02:00
|
|
|
self.assertIsInstance(task.result()[0], asyncio.StreamReader)
|
|
|
|
cleanup_func = task.result()[1]
|
|
|
|
cleanup_func()
|
|
|
|
sock2.close()
|
|
|
|
|
|
|
|
# run socket cleanup functions
|
|
|
|
loop.stop()
|
|
|
|
loop.run_forever()
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
def test_021_get_events_reader_local_vm(self):
|
|
|
|
self.app.qubesd_connection_type = 'socket'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
sock1, sock2 = socket.socketpair()
|
|
|
|
vm = unittest.mock.Mock()
|
|
|
|
vm.name = 'test-vm'
|
|
|
|
with unittest.mock.patch('asyncio.open_unix_connection',
|
|
|
|
lambda path: self.mock_open_unix_connection(
|
2017-05-11 23:21:04 +02:00
|
|
|
qubesadmin.config.QUBESD_SOCKET, sock1, path)):
|
2017-04-14 22:02:43 +02:00
|
|
|
task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
|
|
|
|
reader = asyncio.ensure_future(loop.run_in_executor(None,
|
|
|
|
self.read_all, sock2))
|
|
|
|
loop.run_until_complete(asyncio.wait([task, reader]))
|
|
|
|
self.assertEqual(reader.result(),
|
2017-05-12 19:36:03 +02:00
|
|
|
b'dom0\0admin.Events\0test-vm\0\0')
|
2017-04-14 22:02:43 +02:00
|
|
|
self.assertIsInstance(task.result()[0], asyncio.StreamReader)
|
|
|
|
cleanup_func = task.result()[1]
|
|
|
|
cleanup_func()
|
|
|
|
sock2.close()
|
|
|
|
|
|
|
|
# run socket cleanup functions
|
|
|
|
loop.stop()
|
|
|
|
loop.run_forever()
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def mock_coroutine(self, mock, *args, **kwargs):
|
|
|
|
return mock(*args, **kwargs)
|
|
|
|
|
|
|
|
def test_022_get_events_reader_remote(self):
|
|
|
|
self.app.qubesd_connection_type = 'qrexec'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
mock_proc = unittest.mock.Mock()
|
|
|
|
with unittest.mock.patch('asyncio.create_subprocess_exec',
|
|
|
|
lambda *args, **kwargs: self.mock_coroutine(mock_proc,
|
|
|
|
*args, **kwargs)):
|
|
|
|
task = asyncio.ensure_future(self.dispatcher._get_events_reader())
|
|
|
|
loop.run_until_complete(task)
|
|
|
|
self.assertEqual(mock_proc.mock_calls, [
|
2017-06-25 14:01:14 +02:00
|
|
|
unittest.mock.call('qrexec-client-vm', 'dom0',
|
|
|
|
'admin.Events', stdin=subprocess.PIPE,
|
2017-04-14 22:02:43 +02:00
|
|
|
stdout=subprocess.PIPE),
|
|
|
|
unittest.mock.call().stdin.write_eof()
|
|
|
|
])
|
|
|
|
self.assertEqual(task.result()[0], mock_proc().stdout)
|
|
|
|
cleanup_func = task.result()[1]
|
|
|
|
cleanup_func()
|
|
|
|
unittest.mock.call().kill.assert_called_once_with()
|
|
|
|
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
def test_023_get_events_reader_remote_vm(self):
|
|
|
|
self.app.qubesd_connection_type = 'qrexec'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
mock_proc = unittest.mock.Mock()
|
|
|
|
vm = unittest.mock.Mock()
|
|
|
|
vm.name = 'test-vm'
|
|
|
|
with unittest.mock.patch('asyncio.create_subprocess_exec',
|
|
|
|
lambda *args, **kwargs: self.mock_coroutine(mock_proc,
|
|
|
|
*args, **kwargs)):
|
|
|
|
task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
|
|
|
|
loop.run_until_complete(task)
|
|
|
|
self.assertEqual(mock_proc.mock_calls, [
|
2017-06-25 14:01:14 +02:00
|
|
|
unittest.mock.call('qrexec-client-vm', 'test-vm',
|
|
|
|
'admin.Events', stdin=subprocess.PIPE,
|
2017-04-14 22:02:43 +02:00
|
|
|
stdout=subprocess.PIPE),
|
|
|
|
unittest.mock.call().stdin.write_eof()
|
|
|
|
])
|
|
|
|
self.assertEqual(task.result()[0], mock_proc().stdout)
|
|
|
|
cleanup_func = task.result()[1]
|
|
|
|
cleanup_func()
|
|
|
|
unittest.mock.call().kill.assert_called_once_with()
|
|
|
|
|
|
|
|
loop.close()
|
2018-09-18 21:55:32 +02:00
|
|
|
|
|
|
|
def test_030_events_device(self):
|
|
|
|
handler = unittest.mock.Mock()
|
|
|
|
self.dispatcher.add_handler('device-attach:test', handler)
|
|
|
|
self.dispatcher.handle('test-vm', 'device-attach:test',
|
|
|
|
device='test-vm2:dev', options='{}')
|
|
|
|
vm = self.app.domains.get_blind('test-vm')
|
|
|
|
dev = self.app.domains.get_blind('test-vm2').devices['test']['dev']
|
|
|
|
handler.assert_called_once_with(vm, 'device-attach:test', device=dev,
|
|
|
|
options='{}')
|