2017-02-24 00:14:13 +01: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
|
2017-03-08 21:53:46 +01:00
|
|
|
# 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
|
2017-02-24 00:14:13 +01:00
|
|
|
# (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
|
2017-03-08 21:53:46 +01:00
|
|
|
# GNU Lesser General Public License for more details.
|
2017-02-24 00:14:13 +01:00
|
|
|
#
|
2017-03-08 21:53:46 +01:00
|
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
2017-02-24 00:14:13 +01:00
|
|
|
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2017-02-25 20:56:31 +01:00
|
|
|
'''Qubes VM objects.'''
|
|
|
|
|
2017-04-15 20:10:04 +02:00
|
|
|
import logging
|
2019-02-28 04:37:59 +01:00
|
|
|
import shlex
|
2017-06-21 05:05:35 +02:00
|
|
|
|
|
|
|
import subprocess
|
2018-02-13 19:01:29 +01:00
|
|
|
import warnings
|
2017-06-21 05:05:35 +02:00
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin.base
|
|
|
|
import qubesadmin.exc
|
|
|
|
import qubesadmin.storage
|
|
|
|
import qubesadmin.features
|
|
|
|
import qubesadmin.devices
|
|
|
|
import qubesadmin.firewall
|
2017-06-19 01:55:49 +02:00
|
|
|
import qubesadmin.tags
|
2017-02-24 00:14:13 +01:00
|
|
|
|
2019-03-07 03:05:03 +01:00
|
|
|
if not hasattr(shlex, 'quote'):
|
|
|
|
# python2 compat
|
|
|
|
import pipes
|
|
|
|
shlex.quote = pipes.quote
|
2017-02-24 00:14:13 +01:00
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
class QubesVM(qubesadmin.base.PropertyHolder):
|
2017-02-25 20:56:31 +01:00
|
|
|
'''Qubes domain.'''
|
2017-04-15 20:10:04 +02:00
|
|
|
|
|
|
|
log = None
|
|
|
|
|
2017-06-19 01:55:49 +02:00
|
|
|
tags = None
|
|
|
|
|
2017-04-15 13:53:40 +02:00
|
|
|
features = None
|
|
|
|
|
2017-04-30 02:57:19 +02:00
|
|
|
devices = None
|
|
|
|
|
2017-04-30 22:56:22 +02:00
|
|
|
firewall = None
|
|
|
|
|
2017-10-02 20:43:01 +02:00
|
|
|
def __init__(self, app, name, klass=None):
|
2017-05-12 19:36:03 +02:00
|
|
|
super(QubesVM, self).__init__(app, 'admin.vm.property.', name)
|
2017-03-01 16:39:12 +01:00
|
|
|
self._volumes = None
|
2017-10-02 20:43:01 +02:00
|
|
|
self._klass = klass
|
2017-04-15 20:10:04 +02:00
|
|
|
self.log = logging.getLogger(name)
|
2017-06-19 01:55:49 +02:00
|
|
|
self.tags = qubesadmin.tags.Tags(self)
|
2017-05-11 23:21:04 +02:00
|
|
|
self.features = qubesadmin.features.Features(self)
|
|
|
|
self.devices = qubesadmin.devices.DeviceManager(self)
|
|
|
|
self.firewall = qubesadmin.firewall.Firewall(self)
|
2017-02-24 01:38:47 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2017-02-25 01:47:13 +01:00
|
|
|
'''Domain name'''
|
2017-02-24 01:38:47 +01:00
|
|
|
return self._method_dest
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
def name(self, new_value):
|
|
|
|
self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Set',
|
|
|
|
'name',
|
|
|
|
str(new_value).encode('utf-8'))
|
|
|
|
self._method_dest = new_value
|
2017-03-01 16:39:12 +01:00
|
|
|
self._volumes = None
|
2017-02-24 01:38:47 +01:00
|
|
|
self.app.domains.clear_cache()
|
2017-02-25 01:47:13 +01:00
|
|
|
|
2017-03-12 00:42:50 +01:00
|
|
|
def __str__(self):
|
|
|
|
return self._method_dest
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
if isinstance(other, QubesVM):
|
|
|
|
return self.name < other.name
|
|
|
|
return NotImplemented
|
|
|
|
|
2017-05-18 09:51:58 +02:00
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, QubesVM):
|
|
|
|
return self.name == other.name
|
2018-07-16 02:25:25 +02:00
|
|
|
if isinstance(other, str):
|
2017-05-18 09:51:58 +02:00
|
|
|
return self.name == other
|
|
|
|
return NotImplemented
|
|
|
|
|
2017-07-06 21:53:45 +02:00
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.name)
|
|
|
|
|
2017-02-25 01:47:13 +01:00
|
|
|
def start(self):
|
|
|
|
'''
|
|
|
|
Start domain.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
'''
|
2017-05-12 19:36:03 +02:00
|
|
|
self.qubesd_call(self._method_dest, 'admin.vm.Start')
|
2017-02-25 01:47:13 +01:00
|
|
|
|
2017-03-09 02:26:51 +01:00
|
|
|
def shutdown(self, force=False):
|
2017-02-25 01:47:13 +01:00
|
|
|
'''
|
|
|
|
Shutdown domain.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
'''
|
|
|
|
# TODO: force parameter
|
|
|
|
# TODO: wait parameter (using event?)
|
2017-03-09 02:26:51 +01:00
|
|
|
if force:
|
|
|
|
raise NotImplementedError
|
2017-05-12 19:36:03 +02:00
|
|
|
self.qubesd_call(self._method_dest, 'admin.vm.Shutdown')
|
2017-02-25 01:47:13 +01:00
|
|
|
|
|
|
|
def kill(self):
|
|
|
|
'''
|
|
|
|
Kill domain (forcefuly shutdown).
|
|
|
|
|
|
|
|
:return:
|
|
|
|
'''
|
2017-05-12 19:36:03 +02:00
|
|
|
self.qubesd_call(self._method_dest, 'admin.vm.Kill')
|
2017-02-25 01:47:13 +01:00
|
|
|
|
2018-02-13 19:01:29 +01:00
|
|
|
def force_shutdown(self):
|
|
|
|
'''Deprecated alias for :py:meth:`kill`'''
|
|
|
|
warnings.warn(
|
|
|
|
'Call to deprecated function force_shutdown(), use kill() instead',
|
|
|
|
DeprecationWarning, stacklevel=2)
|
|
|
|
return self.kill()
|
|
|
|
|
2017-02-25 01:47:13 +01:00
|
|
|
def pause(self):
|
|
|
|
'''
|
|
|
|
Pause domain.
|
|
|
|
|
|
|
|
Pause its execution without any prior notification.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
'''
|
2017-05-12 19:36:03 +02:00
|
|
|
self.qubesd_call(self._method_dest, 'admin.vm.Pause')
|
2017-02-25 01:47:13 +01:00
|
|
|
|
|
|
|
def unpause(self):
|
|
|
|
'''
|
|
|
|
Unpause domain.
|
|
|
|
|
|
|
|
Opposite to :py:meth:`pause`.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
'''
|
2017-05-12 19:36:03 +02:00
|
|
|
self.qubesd_call(self._method_dest, 'admin.vm.Unpause')
|
2017-02-25 01:47:13 +01:00
|
|
|
|
2017-03-11 01:52:26 +01:00
|
|
|
def get_power_state(self):
|
|
|
|
'''Return power state description string.
|
|
|
|
|
|
|
|
Return value may be one of those:
|
|
|
|
|
|
|
|
=============== ========================================================
|
|
|
|
return value meaning
|
|
|
|
=============== ========================================================
|
|
|
|
``'Halted'`` Machine is not active.
|
|
|
|
``'Transient'`` Machine is running, but does not have :program:`guid`
|
|
|
|
or :program:`qrexec` available.
|
|
|
|
``'Running'`` Machine is ready and running.
|
|
|
|
``'Paused'`` Machine is paused.
|
|
|
|
``'Suspended'`` Machine is S3-suspended.
|
|
|
|
``'Halting'`` Machine is in process of shutting down (OS shutdown).
|
|
|
|
``'Dying'`` Machine is in process of shutting down (cleanup).
|
|
|
|
``'Crashed'`` Machine crashed and is unusable.
|
|
|
|
``'NA'`` Machine is in unknown state.
|
|
|
|
=============== ========================================================
|
|
|
|
|
|
|
|
.. seealso::
|
|
|
|
|
|
|
|
http://wiki.libvirt.org/page/VM_lifecycle
|
|
|
|
Description of VM life cycle from the point of view of libvirt.
|
|
|
|
|
|
|
|
https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState
|
|
|
|
Libvirt's enum describing precise state of a domain.
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
2017-10-16 01:20:27 +02:00
|
|
|
try:
|
|
|
|
vm_list_info = [line
|
|
|
|
for line in self.qubesd_call(
|
|
|
|
self._method_dest, 'admin.vm.List', None, None
|
|
|
|
).decode('ascii').split('\n')
|
|
|
|
if line.startswith(self._method_dest+' ')]
|
|
|
|
except qubesadmin.exc.QubesDaemonNoResponseError:
|
|
|
|
return 'NA'
|
2017-06-08 14:01:04 +02:00
|
|
|
assert len(vm_list_info) == 1
|
2017-03-11 01:52:26 +01:00
|
|
|
# name class=... state=... other=...
|
2017-06-08 14:01:04 +02:00
|
|
|
# NOTE: when querying dom0, we get whole list
|
|
|
|
vm_state = vm_list_info[0].strip().partition('state=')[2].split(' ')[0]
|
2017-03-11 01:52:26 +01:00
|
|
|
return vm_state
|
|
|
|
|
2017-04-15 14:22:30 +02:00
|
|
|
|
|
|
|
def is_halted(self):
|
|
|
|
''' Check whether this domain's state is 'Halted'
|
|
|
|
:returns: :py:obj:`True` if this domain is halted, \
|
|
|
|
:py:obj:`False` otherwise.
|
|
|
|
:rtype: bool
|
|
|
|
'''
|
|
|
|
return self.get_power_state() == 'Halted'
|
|
|
|
|
|
|
|
def is_paused(self):
|
|
|
|
'''Check whether this domain is paused.
|
|
|
|
|
|
|
|
:returns: :py:obj:`True` if this domain is paused, \
|
|
|
|
:py:obj:`False` otherwise.
|
|
|
|
:rtype: bool
|
|
|
|
'''
|
|
|
|
|
|
|
|
return self.get_power_state() == 'Paused'
|
|
|
|
|
|
|
|
def is_running(self):
|
|
|
|
'''Check whether this domain is running.
|
|
|
|
|
|
|
|
:returns: :py:obj:`True` if this domain is started, \
|
|
|
|
:py:obj:`False` otherwise.
|
|
|
|
:rtype: bool
|
|
|
|
'''
|
|
|
|
|
2017-10-16 01:20:27 +02:00
|
|
|
return self.get_power_state() not in ('Halted', 'NA')
|
2017-04-15 14:22:30 +02:00
|
|
|
|
2017-05-19 17:33:12 +02:00
|
|
|
def is_networked(self):
|
|
|
|
'''Check whether this VM can reach network (firewall notwithstanding).
|
|
|
|
|
|
|
|
:returns: :py:obj:`True` if is machine can reach network, \
|
|
|
|
:py:obj:`False` otherwise.
|
|
|
|
:rtype: bool
|
|
|
|
'''
|
|
|
|
|
|
|
|
if self.provides_network:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return self.netvm is not None
|
|
|
|
|
2017-03-01 16:39:12 +01:00
|
|
|
@property
|
|
|
|
def volumes(self):
|
|
|
|
'''VM disk volumes'''
|
|
|
|
if self._volumes is None:
|
|
|
|
volumes_list = self.qubesd_call(
|
2017-05-12 19:36:03 +02:00
|
|
|
self._method_dest, 'admin.vm.volume.List')
|
2017-03-01 16:39:12 +01:00
|
|
|
self._volumes = {}
|
|
|
|
for volname in volumes_list.decode('ascii').splitlines():
|
|
|
|
if not volname:
|
|
|
|
continue
|
2017-05-11 23:21:04 +02:00
|
|
|
self._volumes[volname] = qubesadmin.storage.Volume(self.app,
|
2017-03-01 16:39:12 +01:00
|
|
|
vm=self.name, vm_name=volname)
|
|
|
|
return self._volumes
|
2017-02-28 01:34:09 +01:00
|
|
|
|
2017-07-29 04:50:19 +02:00
|
|
|
def get_disk_utilization(self):
|
|
|
|
'''Get total disk usage of the VM'''
|
|
|
|
return sum(vol.usage for vol in self.volumes.values())
|
|
|
|
|
2017-04-15 20:11:04 +02:00
|
|
|
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:
|
2017-06-21 05:05:35 +02:00
|
|
|
exc = subprocess.CalledProcessError(p.returncode, service)
|
|
|
|
# Python < 3.5 didn't have those
|
|
|
|
exc.output, exc.stderr = stdouterr
|
|
|
|
raise exc
|
2017-04-15 20:11:04 +02:00
|
|
|
|
|
|
|
return stdouterr
|
|
|
|
|
2018-07-30 18:21:06 +02:00
|
|
|
def prepare_input_for_vmshell(self, command, input=None):
|
2017-04-15 20:11:04 +02:00
|
|
|
'''Prepare shell input for the given command and optional (real) input
|
|
|
|
''' # pylint: disable=redefined-builtin
|
|
|
|
if input is None:
|
|
|
|
input = b''
|
2018-07-30 18:21:06 +02:00
|
|
|
close_shell_suffix = b'; exit\n'
|
|
|
|
if self.features.check_with_template('os', 'Linux') == 'Windows':
|
|
|
|
close_shell_suffix = b'& exit\n'
|
2017-05-18 09:54:26 +02:00
|
|
|
return b''.join((command.rstrip('\n').encode('utf-8'),
|
2018-07-30 18:21:06 +02:00
|
|
|
close_shell_suffix, input))
|
2017-04-15 20:11:04 +02:00
|
|
|
|
|
|
|
def run(self, command, input=None, **kwargs):
|
|
|
|
'''Run a shell command inside the domain using qubes.VMShell qrexec.
|
|
|
|
|
|
|
|
''' # pylint: disable=redefined-builtin
|
2017-06-21 05:05:35 +02:00
|
|
|
try:
|
|
|
|
return self.run_service_for_stdio('qubes.VMShell',
|
|
|
|
input=self.prepare_input_for_vmshell(command, input), **kwargs)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
e.cmd = command
|
|
|
|
raise e
|
|
|
|
|
2019-02-28 04:37:59 +01:00
|
|
|
def run_with_args(self, *args, **kwargs):
|
|
|
|
'''Run a single command inside the domain using qubes.VMShell qrexec.
|
|
|
|
|
|
|
|
This method execute a single command, without interpreting any shell
|
|
|
|
special characters.
|
|
|
|
|
|
|
|
''' # pylint: disable=redefined-builtin
|
|
|
|
return self.run(' '.join(shlex.quote(arg) for arg in args), **kwargs)
|
|
|
|
|
2017-10-02 20:50:33 +02:00
|
|
|
@property
|
|
|
|
def appvms(self):
|
|
|
|
''' Returns a generator containing all domains based on the current
|
|
|
|
TemplateVM.
|
|
|
|
|
|
|
|
Do not check vm type of self, core (including its extentions) have
|
|
|
|
ultimate control what can be a template of what.
|
|
|
|
'''
|
|
|
|
for vm in self.app.domains:
|
|
|
|
try:
|
|
|
|
if vm.template == self:
|
|
|
|
yield vm
|
2018-01-11 01:55:54 +01:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@property
|
|
|
|
def connected_vms(self):
|
|
|
|
''' Return a generator containing all domains connected to the current
|
|
|
|
NetVM.
|
|
|
|
'''
|
|
|
|
for vm in self.app.domains:
|
|
|
|
try:
|
|
|
|
if vm.netvm == self:
|
|
|
|
yield vm
|
2017-10-02 20:50:33 +02:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
2017-10-02 20:43:01 +02:00
|
|
|
@property
|
|
|
|
def klass(self):
|
|
|
|
''' Qube class '''
|
|
|
|
# use cached value if available
|
|
|
|
if self._klass is None:
|
|
|
|
# pylint: disable=no-member
|
|
|
|
self._klass = super(QubesVM, self).klass
|
|
|
|
return self._klass
|
2017-04-15 20:11:04 +02:00
|
|
|
|
2017-08-06 12:22:47 +02:00
|
|
|
class DispVMWrapper(QubesVM):
|
|
|
|
'''Wrapper class for new DispVM, supporting only service call
|
|
|
|
|
|
|
|
Note that when running in dom0, one need to manually kill the DispVM after
|
|
|
|
service call ends.
|
|
|
|
'''
|
|
|
|
|
|
|
|
def run_service(self, service, **kwargs):
|
|
|
|
if self.app.qubesd_connection_type == 'socket':
|
|
|
|
# create dispvm at service call
|
|
|
|
if self._method_dest.startswith('$dispvm'):
|
|
|
|
if self._method_dest.startswith('$dispvm:'):
|
|
|
|
method_dest = self._method_dest[len('$dispvm:'):]
|
|
|
|
else:
|
|
|
|
method_dest = 'dom0'
|
|
|
|
dispvm = self.app.qubesd_call(method_dest,
|
|
|
|
'admin.vm.CreateDisposable')
|
|
|
|
dispvm = dispvm.decode('ascii')
|
|
|
|
self._method_dest = dispvm
|
2018-01-12 19:57:45 +01:00
|
|
|
# Service call may wait for session start, give it more time
|
|
|
|
# than default 5s
|
|
|
|
kwargs['connect_timeout'] = self.qrexec_timeout
|
2017-08-06 12:22:47 +02:00
|
|
|
return super(DispVMWrapper, self).run_service(service, **kwargs)
|
|
|
|
|
|
|
|
def cleanup(self):
|
|
|
|
'''Cleanup after DispVM usage'''
|
|
|
|
# in 'remote' case nothing is needed, as DispVM is cleaned up
|
|
|
|
# automatically
|
2017-08-12 15:15:06 +02:00
|
|
|
if self.app.qubesd_connection_type == 'socket' and \
|
|
|
|
not self._method_dest.startswith('$dispvm'):
|
2017-08-06 12:22:47 +02:00
|
|
|
try:
|
|
|
|
self.kill()
|
|
|
|
except qubesadmin.exc.QubesVMNotRunningError:
|
|
|
|
pass
|
|
|
|
|
2017-02-28 01:34:09 +01:00
|
|
|
|
|
|
|
class DispVM(QubesVM):
|
|
|
|
'''Disposable VM'''
|
2017-08-06 12:22:47 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_appvm(cls, app, appvm):
|
|
|
|
'''Returns a wrapper for calling service in a new DispVM based on given
|
|
|
|
AppVM. If *appvm* is none, use default DispVM template'''
|
|
|
|
|
|
|
|
if appvm:
|
|
|
|
method_dest = '$dispvm:' + str(appvm)
|
|
|
|
else:
|
|
|
|
method_dest = '$dispvm'
|
|
|
|
|
|
|
|
wrapper = DispVMWrapper(app, method_dest)
|
|
|
|
return wrapper
|