diff --git a/qubesmgmt/app.py b/qubesmgmt/app.py index df34f36..adfc7ad 100644 --- a/qubesmgmt/app.py +++ b/qubesmgmt/app.py @@ -22,7 +22,7 @@ ''' Main Qubes() class and related classes. ''' - +import shlex import socket import subprocess @@ -179,6 +179,23 @@ class QubesBase(qubesmgmt.base.PropertyHolder): ''' Remove a storage pool ''' self.qubesd_call('dom0', 'mgmt.pool.Remove', name, None) + def run_service(self, dest, service, filter_esc=False, user=None, + localcmd=None, **kwargs): + '''Run qrexec service in a given destination + + *kwargs* are passed verbatim to :py:meth:`subprocess.Popen`. + + :param str dest: Destination - may be a VM name or empty + string for default (for a given service) + :param str service: service name + :param bool filter_esc: filter escape sequences to protect terminal \ + emulator + :param str user: username to run service as + :param str localcmd: Command to connect stdin/stdout to + :rtype: subprocess.Popen + ''' + raise NotImplementedError + class QubesLocal(QubesBase): '''Application object communicating through local socket. @@ -209,6 +226,41 @@ class QubesLocal(QubesBase): return_data = client_socket.makefile('rb').read() return self._parse_qubesd_response(return_data) + def run_service(self, dest, service, filter_esc=False, user=None, + localcmd=None, **kwargs): + '''Run qrexec service in a given destination + + :param str dest: Destination - may be a VM name or empty + string for default (for a given service) + :param str service: service name + :param bool filter_esc: filter escape sequences to protect terminal \ + emulator + :param str user: username to run service as + :param str localcmd: Command to connect stdin/stdout to + :rtype: subprocess.Popen + ''' + + if not dest: + raise ValueError('Empty destination name allowed only from a VM') + try: + self.qubesd_call(dest, 'mgmt.vm.Start') + except qubesmgmt.exc.QubesVMNotHaltedError: + pass + qrexec_opts = ['-d', dest] + if filter_esc: + qrexec_opts.extend(['-t', '-T']) + if localcmd: + qrexec_opts.extend(['-l', localcmd]) + if user is None: + user = 'DEFAULT' + kwargs.setdefault('stdin', subprocess.PIPE) + kwargs.setdefault('stdout', subprocess.PIPE) + kwargs.setdefault('stderr', subprocess.PIPE) + proc = subprocess.Popen([qubesmgmt.config.QREXEC_CLIENT] + + qrexec_opts + ['{}:QUBESRPC {} dom0'.format(user, service)], + **kwargs) + return proc + class QubesRemote(QubesBase): '''Application object communicating through qrexec services. @@ -232,3 +284,30 @@ class QubesRemote(QubesBase): stderr.decode()) return self._parse_qubesd_response(stdout) + + def run_service(self, dest, service, filter_esc=False, user=None, + localcmd=None, **kwargs): + '''Run qrexec service in a given destination + + :param str dest: Destination - may be a VM name or empty + string for default (for a given service) + :param str service: service name + :param bool filter_esc: filter escape sequences to protect terminal \ + emulator + :param str user: username to run service as + :param str localcmd: Command to connect stdin/stdout to + :rtype: subprocess.Popen + ''' + if filter_esc: + raise NotImplementedError( + 'filter_esc not implemented for calls from VM') + if user: + raise ValueError( + 'non-default user not possible for calls from VM') + kwargs.setdefault('stdin', subprocess.PIPE) + kwargs.setdefault('stdout', subprocess.PIPE) + kwargs.setdefault('stderr', subprocess.PIPE) + proc = subprocess.Popen([qubesmgmt.config.QREXEC_CLIENT_VM, + dest, service] + shlex.split(localcmd) if localcmd else [], + **kwargs) + return proc diff --git a/qubesmgmt/config.py b/qubesmgmt/config.py index 83c3d2d..22cb271 100644 --- a/qubesmgmt/config.py +++ b/qubesmgmt/config.py @@ -22,3 +22,5 @@ #: path to qubesd socket QUBESD_SOCKET = '/var/run/qubesd.sock' +QREXEC_CLIENT = '/usr/lib/qubes/qrexec-client' +QREXEC_CLIENT_VM = '/usr/bin/qrexec-client-vm' diff --git a/qubesmgmt/vm/__init__.py b/qubesmgmt/vm/__init__.py index 03a7119..65def6f 100644 --- a/qubesmgmt/vm/__init__.py +++ b/qubesmgmt/vm/__init__.py @@ -22,6 +22,7 @@ import logging import qubesmgmt.base +import qubesmgmt.exc import qubesmgmt.storage import qubesmgmt.features @@ -213,6 +214,54 @@ class QubesVM(qubesmgmt.base.PropertyHolder): vm=self.name, vm_name=volname) return self._volumes + def run_service(self, service, **kwargs): + '''Run service on this VM + + :param str service: service name + :rtype: subprocess.Popen + ''' + return self.app.run_service(self._method_dest, service, **kwargs) + + def run_service_for_stdio(self, service, input=None, **kwargs): + '''Run a service, pass an optional input and return (stdout, stderr). + + Raises an exception if return code != 0. + + *args* and *kwargs* are passed verbatim to :py:meth:`run_service`. + + .. warning:: + There are some combinations if stdio-related *kwargs*, which are + not filtered for problems originating between the keyboard and the + chair. + ''' # pylint: disable=redefined-builtin + p = self.run_service(service, **kwargs) + + # this one is actually a tuple, but there is no need to unpack it + stdouterr = p.communicate(input=input) + + if p.returncode: + raise qubesmgmt.exc.QubesVMError(self, + 'service {!r} failed with retcode {!r}; ' + 'stdout={!r} stderr={!r}'.format( + service, p.returncode, *stdouterr)) + + return stdouterr + + @staticmethod + def prepare_input_for_vmshell(command, input=None): + '''Prepare shell input for the given command and optional (real) input + ''' # pylint: disable=redefined-builtin + if input is None: + input = b'' + return b''.join((command.rstrip('\n').encode('utf-8'), b'\n', input)) + + def run(self, command, input=None, **kwargs): + '''Run a shell command inside the domain using qubes.VMShell qrexec. + + ''' # pylint: disable=redefined-builtin + return self.run_service_for_stdio('qubes.VMShell', + input=self.prepare_input_for_vmshell(command, input), **kwargs) + # pylint: disable=abstract-method class AdminVM(QubesVM): '''Dom0'''