Merge branch 'devel-4'
* devel-4: tools/qvm-start-gui: multiple fixes vm: raise CalledProcessError instead of QubesVMError on failed service call events: improve handling qubesd restart
This commit is contained in:
commit
3cf5840d7a
@ -118,7 +118,8 @@ class EventsDispatcher(object):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
yield from self._listen_for_events(vm)
|
yield from self._listen_for_events(vm)
|
||||||
except ConnectionRefusedError:
|
except (ConnectionRefusedError, ConnectionResetError,
|
||||||
|
FileNotFoundError):
|
||||||
pass
|
pass
|
||||||
if not reconnect:
|
if not reconnect:
|
||||||
break
|
break
|
||||||
|
@ -142,18 +142,30 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
|
|
||||||
@unittest.mock.patch('asyncio.create_subprocess_exec')
|
@unittest.mock.patch('asyncio.create_subprocess_exec')
|
||||||
def test_020_start_gui_for_vm(self, proc_mock):
|
def test_020_start_gui_for_vm(self, proc_mock):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
self.addCleanup(loop.close)
|
||||||
|
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('dom0', 'admin.vm.List', None, None)] = \
|
('dom0', 'admin.vm.List', None, None)] = \
|
||||||
b'0\x00test-vm class=AppVM state=Running\n'
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.List', None, None)] = \
|
||||||
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
||||||
b'0\x00default=False type=int 3000'
|
b'0\x00default=False type=int 3000'
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('test-vm', 'admin.vm.property.Get', 'hvm', None)] = \
|
('test-vm', 'admin.vm.property.Get', 'hvm', None)] = \
|
||||||
b'0\x00default=False type=bool False'
|
b'0\x00default=False type=bool False'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.feature.CheckWithTemplate',
|
||||||
|
'no-monitor-layout', None)] = \
|
||||||
|
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
|
||||||
with unittest.mock.patch.object(self.launcher,
|
with unittest.mock.patch.object(self.launcher,
|
||||||
'common_guid_args', lambda vm: []):
|
'common_guid_args', lambda vm: []):
|
||||||
self.launcher.start_gui_for_vm(self.app.domains['test-vm'])
|
loop.run_until_complete(self.launcher.start_gui_for_vm(
|
||||||
|
self.app.domains['test-vm']))
|
||||||
# common arguments dropped for simplicity
|
# common arguments dropped for simplicity
|
||||||
proc_mock.assert_called_once_with('-d', '3000')
|
proc_mock.assert_called_once_with('-d', '3000')
|
||||||
|
|
||||||
@ -161,9 +173,16 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
|
|
||||||
@unittest.mock.patch('asyncio.create_subprocess_exec')
|
@unittest.mock.patch('asyncio.create_subprocess_exec')
|
||||||
def test_021_start_gui_for_vm_hvm(self, proc_mock):
|
def test_021_start_gui_for_vm_hvm(self, proc_mock):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
self.addCleanup(loop.close)
|
||||||
|
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('dom0', 'admin.vm.List', None, None)] = \
|
('dom0', 'admin.vm.List', None, None)] = \
|
||||||
b'0\x00test-vm class=AppVM state=Running\n'
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.List', None, None)] = \
|
||||||
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
||||||
b'0\x00default=False type=int 3000'
|
b'0\x00default=False type=int 3000'
|
||||||
@ -180,18 +199,30 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard',
|
('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard',
|
||||||
None)] = \
|
None)] = \
|
||||||
b'0\x00True'
|
b'0\x00True'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.feature.CheckWithTemplate',
|
||||||
|
'no-monitor-layout', None)] = \
|
||||||
|
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
|
||||||
with unittest.mock.patch.object(self.launcher,
|
with unittest.mock.patch.object(self.launcher,
|
||||||
'common_guid_args', lambda vm: []):
|
'common_guid_args', lambda vm: []):
|
||||||
self.launcher.start_gui_for_vm(self.app.domains['test-vm'])
|
loop.run_until_complete(self.launcher.start_gui_for_vm(
|
||||||
|
self.app.domains['test-vm']))
|
||||||
# common arguments dropped for simplicity
|
# common arguments dropped for simplicity
|
||||||
proc_mock.assert_called_once_with('-d', '3000', '-n', '-Q')
|
proc_mock.assert_called_once_with('-d', '3000', '-n', '-Q')
|
||||||
|
|
||||||
self.assertAllCalled()
|
self.assertAllCalled()
|
||||||
|
|
||||||
def test_022_start_gui_for_vm_hvm_stubdom(self):
|
def test_022_start_gui_for_vm_hvm_stubdom(self):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
self.addCleanup(loop.close)
|
||||||
|
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('dom0', 'admin.vm.List', None, None)] = \
|
('dom0', 'admin.vm.List', None, None)] = \
|
||||||
b'0\x00test-vm class=AppVM state=Running\n'
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.List', None, None)] = \
|
||||||
|
b'0\x00test-vm class=AppVM state=Running\n'
|
||||||
self.app.expected_calls[
|
self.app.expected_calls[
|
||||||
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
('test-vm', 'admin.vm.property.Get', 'xid', None)] = \
|
||||||
b'0\x00default=False type=int 3000'
|
b'0\x00default=False type=int 3000'
|
||||||
@ -208,6 +239,10 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard',
|
('test-vm', 'admin.vm.feature.CheckWithTemplate', 'rpc-clipboard',
|
||||||
None)] = \
|
None)] = \
|
||||||
b'0\x00True'
|
b'0\x00True'
|
||||||
|
self.app.expected_calls[
|
||||||
|
('test-vm', 'admin.vm.feature.CheckWithTemplate',
|
||||||
|
'no-monitor-layout', None)] = \
|
||||||
|
b'2\x00QubesFeatureNotFoundError\x00\x00Feature not set\x00'
|
||||||
pidfile = tempfile.NamedTemporaryFile()
|
pidfile = tempfile.NamedTemporaryFile()
|
||||||
pidfile.write(b'1234\n')
|
pidfile.write(b'1234\n')
|
||||||
pidfile.flush()
|
pidfile.flush()
|
||||||
@ -222,7 +257,8 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
mock_proc = patch_proc.start()
|
mock_proc = patch_proc.start()
|
||||||
patch_args.start()
|
patch_args.start()
|
||||||
patch_pidfile.start()
|
patch_pidfile.start()
|
||||||
self.launcher.start_gui_for_vm(self.app.domains['test-vm'])
|
loop.run_until_complete(self.launcher.start_gui_for_vm(
|
||||||
|
self.app.domains['test-vm']))
|
||||||
# common arguments dropped for simplicity
|
# common arguments dropped for simplicity
|
||||||
mock_proc.assert_called_once_with(
|
mock_proc.assert_called_once_with(
|
||||||
'-d', '3000', '-n', '-Q', '-K', '1234')
|
'-d', '3000', '-n', '-Q', '-K', '1234')
|
||||||
@ -252,8 +288,8 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
self.assertAllCalled()
|
self.assertAllCalled()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def mock_coroutine(self, mock, *args):
|
def mock_coroutine(self, mock, *args, **kwargs):
|
||||||
mock(*args)
|
mock(*args, **kwargs)
|
||||||
|
|
||||||
def test_040_start_gui(self):
|
def test_040_start_gui(self):
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
@ -287,8 +323,8 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
mock_start_vm = unittest.mock.Mock()
|
mock_start_vm = unittest.mock.Mock()
|
||||||
mock_start_stubdomain = unittest.mock.Mock()
|
mock_start_stubdomain = unittest.mock.Mock()
|
||||||
patch_start_vm = unittest.mock.patch.object(
|
patch_start_vm = unittest.mock.patch.object(
|
||||||
self.launcher, 'start_gui_for_vm', lambda vm_:
|
self.launcher, 'start_gui_for_vm', functools.partial(
|
||||||
self.mock_coroutine(mock_start_vm, vm_))
|
self.mock_coroutine, mock_start_vm))
|
||||||
patch_start_stubdomain = unittest.mock.patch.object(
|
patch_start_stubdomain = unittest.mock.patch.object(
|
||||||
self.launcher, 'start_gui_for_stubdomain', lambda vm_:
|
self.launcher, 'start_gui_for_stubdomain', lambda vm_:
|
||||||
self.mock_coroutine(mock_start_stubdomain, vm_))
|
self.mock_coroutine(mock_start_stubdomain, vm_))
|
||||||
@ -296,7 +332,7 @@ class TC_00_qvm_start_gui(qubesadmin.tests.QubesTestCase):
|
|||||||
patch_start_vm.start()
|
patch_start_vm.start()
|
||||||
patch_start_stubdomain.start()
|
patch_start_stubdomain.start()
|
||||||
loop.run_until_complete(self.launcher.start_gui(vm))
|
loop.run_until_complete(self.launcher.start_gui(vm))
|
||||||
mock_start_vm.assert_called_once_with(vm)
|
mock_start_vm.assert_called_once_with(vm, monitor_layout=None)
|
||||||
mock_start_stubdomain.assert_called_once_with(vm)
|
mock_start_stubdomain.assert_called_once_with(vm)
|
||||||
finally:
|
finally:
|
||||||
unittest.mock.patch.stopall()
|
unittest.mock.patch.stopall()
|
||||||
|
@ -174,10 +174,15 @@ class GUILauncher(object):
|
|||||||
'''Helper function to construct a pidfile path'''
|
'''Helper function to construct a pidfile path'''
|
||||||
return '/var/run/qubes/guid-running.{}'.format(xid)
|
return '/var/run/qubes/guid-running.{}'.format(xid)
|
||||||
|
|
||||||
def start_gui_for_vm(self, vm):
|
@asyncio.coroutine
|
||||||
|
def start_gui_for_vm(self, vm, monitor_layout=None):
|
||||||
'''Start GUI daemon (qubes-guid) connected directly to a VM
|
'''Start GUI daemon (qubes-guid) connected directly to a VM
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
|
|
||||||
|
:param vm: VM for which start GUI daemon
|
||||||
|
:param monitor_layout: monitor layout to send; if None, fetch it from
|
||||||
|
local X server.
|
||||||
'''
|
'''
|
||||||
guid_cmd = self.common_guid_args(vm)
|
guid_cmd = self.common_guid_args(vm)
|
||||||
guid_cmd.extend(['-d', str(vm.xid)])
|
guid_cmd.extend(['-d', str(vm.xid)])
|
||||||
@ -195,13 +200,19 @@ class GUILauncher(object):
|
|||||||
stubdom_guid_pid = pidfile.read().strip()
|
stubdom_guid_pid = pidfile.read().strip()
|
||||||
guid_cmd += ['-K', stubdom_guid_pid]
|
guid_cmd += ['-K', stubdom_guid_pid]
|
||||||
|
|
||||||
return asyncio.create_subprocess_exec(*guid_cmd)
|
vm.log.info('Starting GUI')
|
||||||
|
|
||||||
|
yield from asyncio.create_subprocess_exec(*guid_cmd)
|
||||||
|
|
||||||
|
yield from self.send_monitor_layout(vm, layout=monitor_layout,
|
||||||
|
startup=True)
|
||||||
|
|
||||||
def start_gui_for_stubdomain(self, vm):
|
def start_gui_for_stubdomain(self, vm):
|
||||||
'''Start GUI daemon (qubes-guid) connected to a stubdomain
|
'''Start GUI daemon (qubes-guid) connected to a stubdomain
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
'''
|
'''
|
||||||
|
vm.log.info('Starting GUI (stubdomain)')
|
||||||
guid_cmd = self.common_guid_args(vm)
|
guid_cmd = self.common_guid_args(vm)
|
||||||
guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)])
|
guid_cmd.extend(['-d', str(vm.stubdom_xid), '-t', str(vm.xid)])
|
||||||
|
|
||||||
@ -220,17 +231,13 @@ class GUILauncher(object):
|
|||||||
if not vm.features.check_with_template('gui', True):
|
if not vm.features.check_with_template('gui', True):
|
||||||
return
|
return
|
||||||
|
|
||||||
vm.log.info('Starting GUI')
|
|
||||||
if vm.hvm:
|
if vm.hvm:
|
||||||
if force_stubdom or not os.path.exists(self.guid_pidfile(vm.xid)):
|
if force_stubdom or not os.path.exists(self.guid_pidfile(vm.xid)):
|
||||||
if not os.path.exists(self.guid_pidfile(vm.stubdom_xid)):
|
if not os.path.exists(self.guid_pidfile(vm.stubdom_xid)):
|
||||||
yield from self.start_gui_for_stubdomain(vm)
|
yield from self.start_gui_for_stubdomain(vm)
|
||||||
|
|
||||||
if not os.path.exists(self.guid_pidfile(vm.xid)):
|
if not os.path.exists(self.guid_pidfile(vm.xid)):
|
||||||
yield from self.start_gui_for_vm(vm)
|
yield from self.start_gui_for_vm(vm, monitor_layout=monitor_layout)
|
||||||
|
|
||||||
yield from self.send_monitor_layout(vm, layout=monitor_layout,
|
|
||||||
startup=True)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def send_monitor_layout(self, vm, layout=None, startup=False):
|
def send_monitor_layout(self, vm, layout=None, startup=False):
|
||||||
@ -267,9 +274,12 @@ class GUILauncher(object):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
yield from asyncio.get_event_loop().run_in_executor(None,
|
yield from asyncio.get_event_loop().run_in_executor(None,
|
||||||
vm.run_service_for_stdio, 'qubes.SetMonitorLayout',
|
vm.run_service_for_stdio, 'qubes.SetMonitorLayout',
|
||||||
''.join(layout).encode())
|
''.join(layout).encode())
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
vm.log.warning('Failed to send monitor layout: %s', e.stderr)
|
||||||
|
|
||||||
def send_monitor_layout_all(self):
|
def send_monitor_layout_all(self):
|
||||||
'''Send monitor layout to all (running) VMs'''
|
'''Send monitor layout to all (running) VMs'''
|
||||||
@ -302,12 +312,21 @@ class GUILauncher(object):
|
|||||||
daemon for domains started before this tool. '''
|
daemon for domains started before this tool. '''
|
||||||
|
|
||||||
monitor_layout = get_monitor_layout()
|
monitor_layout = get_monitor_layout()
|
||||||
|
self.app.domains.clear_cache()
|
||||||
for vm in self.app.domains:
|
for vm in self.app.domains:
|
||||||
if isinstance(vm, qubesadmin.vm.AdminVM):
|
if isinstance(vm, qubesadmin.vm.AdminVM):
|
||||||
continue
|
continue
|
||||||
if vm.is_running():
|
if not vm.features.check_with_template('gui', True):
|
||||||
|
continue
|
||||||
|
power_state = vm.get_power_state()
|
||||||
|
if power_state == 'Running':
|
||||||
asyncio.ensure_future(self.start_gui(vm,
|
asyncio.ensure_future(self.start_gui(vm,
|
||||||
monitor_layout=monitor_layout))
|
monitor_layout=monitor_layout))
|
||||||
|
elif power_state == 'Transient':
|
||||||
|
# it is still starting, we'll get 'domain-start' event when
|
||||||
|
# fully started
|
||||||
|
if vm.hvm:
|
||||||
|
asyncio.ensure_future(self.start_gui_for_stubdomain(vm))
|
||||||
|
|
||||||
def register_events(self, events):
|
def register_events(self, events):
|
||||||
'''Register domain startup events in app.events dispatcher'''
|
'''Register domain startup events in app.events dispatcher'''
|
||||||
|
@ -21,6 +21,9 @@
|
|||||||
'''Qubes VM objects.'''
|
'''Qubes VM objects.'''
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import qubesadmin.base
|
import qubesadmin.base
|
||||||
import qubesadmin.exc
|
import qubesadmin.exc
|
||||||
import qubesadmin.storage
|
import qubesadmin.storage
|
||||||
@ -277,10 +280,10 @@ class QubesVM(qubesadmin.base.PropertyHolder):
|
|||||||
stdouterr = p.communicate(input=input)
|
stdouterr = p.communicate(input=input)
|
||||||
|
|
||||||
if p.returncode:
|
if p.returncode:
|
||||||
raise qubesadmin.exc.QubesVMError(
|
exc = subprocess.CalledProcessError(p.returncode, service)
|
||||||
'VM {}: service {!r} failed with retcode {!r}; '
|
# Python < 3.5 didn't have those
|
||||||
'stdout={!r} stderr={!r}'.format(self,
|
exc.output, exc.stderr = stdouterr
|
||||||
service, p.returncode, *stdouterr))
|
raise exc
|
||||||
|
|
||||||
return stdouterr
|
return stdouterr
|
||||||
|
|
||||||
@ -297,8 +300,13 @@ class QubesVM(qubesadmin.base.PropertyHolder):
|
|||||||
'''Run a shell command inside the domain using qubes.VMShell qrexec.
|
'''Run a shell command inside the domain using qubes.VMShell qrexec.
|
||||||
|
|
||||||
''' # pylint: disable=redefined-builtin
|
''' # pylint: disable=redefined-builtin
|
||||||
|
try:
|
||||||
return self.run_service_for_stdio('qubes.VMShell',
|
return self.run_service_for_stdio('qubes.VMShell',
|
||||||
input=self.prepare_input_for_vmshell(command, input), **kwargs)
|
input=self.prepare_input_for_vmshell(command, input), **kwargs)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
e.cmd = command
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
class AdminVM(QubesVM):
|
class AdminVM(QubesVM):
|
||||||
|
Loading…
Reference in New Issue
Block a user