From 98260ff1481ac0f0135e8ed21e6ebace0ac0c787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 21 Sep 2019 03:57:35 +0200 Subject: [PATCH] Add run_service(..., autostart=False) argument This allows to run a service but do not cause a qube to be started it isn't already running. This is especially useful for background / internal calls designed to service a running target VM - if VM is not running, those do not make sense to be called in the first place. Specifically, this will allow qvm-start-gui to avoid re-starting a domain while calling qubes.NotifyMonitorLayout, when a VM is shutdown shortly after its startup. --- qubesadmin/app.py | 28 ++++++++++++++++++++-------- qubesadmin/tests/app.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/qubesadmin/app.py b/qubesadmin/app.py index 440f9fd..3f580e3 100644 --- a/qubesadmin/app.py +++ b/qubesadmin/app.py @@ -487,7 +487,7 @@ class QubesBase(qubesadmin.base.PropertyHolder): 'class: qubesadmin.Qubes()') def run_service(self, dest, service, filter_esc=False, user=None, - localcmd=None, wait=True, **kwargs): + localcmd=None, wait=True, autostart=True, **kwargs): """Run qrexec service in a given destination *kwargs* are passed verbatim to :py:meth:`subprocess.Popen`. @@ -500,6 +500,7 @@ class QubesBase(qubesadmin.base.PropertyHolder): :param str user: username to run service as :param str localcmd: Command to connect stdin/stdout to :param bool wait: Wait service run + :param bool autostart: Automatically start the target VM :rtype: subprocess.Popen """ raise NotImplementedError( @@ -576,7 +577,7 @@ class QubesLocal(QubesBase): return self._parse_qubesd_response(return_data) def run_service(self, dest, service, filter_esc=False, user=None, - localcmd=None, wait=True, **kwargs): + localcmd=None, wait=True, autostart=True, **kwargs): """Run qrexec service in a given destination :param str dest: Destination - may be a VM name or empty @@ -594,10 +595,14 @@ class QubesLocal(QubesBase): raise ValueError('Empty destination name allowed only from a VM') if not wait and localcmd: raise ValueError('wait=False incompatible with localcmd') - try: - self.qubesd_call(dest, 'admin.vm.Start') - except qubesadmin.exc.QubesVMNotHaltedError: - pass + if autostart: + try: + self.qubesd_call(dest, 'admin.vm.Start') + except qubesadmin.exc.QubesVMNotHaltedError: + pass + elif not self.domains.get_blind(dest).is_running(): + raise qubesadmin.exc.QubesVMNotRunningError( + '%s is not running', dest) qrexec_opts = ['-d', dest] if filter_esc: qrexec_opts.extend(['-t']) @@ -665,7 +670,7 @@ class QubesRemote(QubesBase): return self._parse_qubesd_response(stdout) def run_service(self, dest, service, filter_esc=False, user=None, - localcmd=None, wait=True, **kwargs): + localcmd=None, wait=True, autostart=True, **kwargs): """Run qrexec service in a given destination :param str dest: Destination - may be a VM name or empty @@ -678,6 +683,9 @@ class QubesRemote(QubesBase): :param bool wait: wait for process to finish :rtype: subprocess.Popen """ + if not autostart and not dest: + raise ValueError( + 'autostart=False makes sense only with a defined target') if user: raise ValueError( 'non-default user not possible for calls from VM') @@ -686,8 +694,12 @@ class QubesRemote(QubesBase): qrexec_opts = [] if filter_esc: qrexec_opts.extend(['-t']) - if filter_esc or os.isatty(sys.stderr.fileno()): + if filter_esc or ( + os.isatty(sys.stderr.fileno()) and 'stderr' not in kwargs): qrexec_opts.extend(['-T']) + if not autostart and not self.domains.get_blind(dest).is_running(): + raise qubesadmin.exc.QubesVMNotRunningError( + '%s is not running', dest) if not wait: # qrexec-client-vm can only request service calls, which are # started using MSG_EXEC_CMDLINE qrexec protocol message; this diff --git a/qubesadmin/tests/app.py b/qubesadmin/tests/app.py index 143a314..49ca2d7 100644 --- a/qubesadmin/tests/app.py +++ b/qubesadmin/tests/app.py @@ -29,6 +29,7 @@ try: import unittest.mock as mock except ImportError: import mock +from mock import call import tempfile @@ -911,3 +912,30 @@ class TC_30_QubesRemote(unittest.TestCase): '-T', '', 'service.name'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + @mock.patch('os.isatty', lambda fd: fd == 2) + def test_014_run_service_no_autostart1(self): + self.set_proc_stdout( b'0\x00some-vm class=AppVM state=Running\n') + self.app.run_service('some-vm', 'service.name', autostart=False) + self.proc_mock.assert_has_calls([ + call([qubesadmin.config.QREXEC_CLIENT_VM, + 'some-vm', 'admin.vm.List'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE), + call().communicate(None), + call([qubesadmin.config.QREXEC_CLIENT_VM, + '-T', 'some-vm', 'service.name'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE), + ]) + + @mock.patch('os.isatty', lambda fd: fd == 2) + def test_015_run_service_no_autostart2(self): + self.set_proc_stdout( b'0\x00some-vm class=AppVM state=Halted\n') + with self.assertRaises(qubesadmin.exc.QubesVMNotRunningError): + self.app.run_service('some-vm', 'service.name', autostart=False) + self.proc_mock.assert_called_once_with([ + qubesadmin.config.QREXEC_CLIENT_VM, + 'some-vm', 'admin.vm.List'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE)