63ac952803
This should allow importing a volume and changing the size at the same time, without performing the resize operation on original volume first. The internal API has been renamed to internal.vm.volume.ImportBegin to avoid confusion, and for symmetry with ImportEnd. See QubesOS/qubes-issues#5239.
222 lines
7.7 KiB
Python
222 lines
7.7 KiB
Python
# -*- encoding: utf-8 -*-
|
|
#
|
|
# The Qubes OS Project, http://www.qubes-os.org
|
|
#
|
|
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
|
# <marmarek@invisiblethingslab.com>
|
|
#
|
|
# This library 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 library 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 library; if not, see <https://www.gnu.org/licenses/>.
|
|
|
|
''' Internal interface for dom0 components to communicate with qubesd. '''
|
|
|
|
import asyncio
|
|
import json
|
|
import subprocess
|
|
|
|
import qubes.api
|
|
import qubes.api.admin
|
|
import qubes.vm.adminvm
|
|
import qubes.vm.dispvm
|
|
|
|
|
|
class QubesInternalAPI(qubes.api.AbstractQubesAPI):
|
|
''' Communication interface for dom0 components,
|
|
by design the input here is trusted.'''
|
|
|
|
SOCKNAME = '/var/run/qubesd.internal.sock'
|
|
|
|
@qubes.api.method('internal.GetSystemInfo', no_payload=True)
|
|
@asyncio.coroutine
|
|
def getsysteminfo(self):
|
|
self.enforce(self.dest.name == 'dom0')
|
|
self.enforce(not self.arg)
|
|
|
|
system_info = {'domains': {
|
|
domain.name: {
|
|
'tags': list(domain.tags),
|
|
'type': domain.__class__.__name__,
|
|
'template_for_dispvms':
|
|
getattr(domain, 'template_for_dispvms', False),
|
|
'default_dispvm': (str(domain.default_dispvm) if
|
|
getattr(domain, 'default_dispvm', None) else None),
|
|
'icon': str(domain.label.icon),
|
|
} for domain in self.app.domains
|
|
}}
|
|
|
|
return json.dumps(system_info)
|
|
|
|
@qubes.api.method('internal.vm.volume.ImportBegin',
|
|
scope='local', write=True)
|
|
@asyncio.coroutine
|
|
def vm_volume_import(self, untrusted_payload):
|
|
"""Begin importing volume data. Payload is either size of new data
|
|
in bytes, or empty. If empty, the current volume's size will be used.
|
|
Returns size and path to where data should be written.
|
|
|
|
Triggered by scripts in /etc/qubes-rpc:
|
|
admin.vm.volume.Import, admin.vm.volume.ImportWithSize.
|
|
|
|
When the script finish importing, it will trigger
|
|
internal.vm.volume.ImportEnd (with either b'ok' or b'fail' as a
|
|
payload) and response from that call will be actually send to the
|
|
caller.
|
|
"""
|
|
self.enforce(self.arg in self.dest.volumes.keys())
|
|
|
|
if untrusted_payload:
|
|
original_method = 'admin.vm.volume.ImportWithSize'
|
|
else:
|
|
original_method = 'admin.vm.volume.Import'
|
|
self.src.fire_event(
|
|
'admin-permission:' + original_method,
|
|
pre_event=True, dest=self.dest, arg=self.arg)
|
|
|
|
if not self.dest.is_halted():
|
|
raise qubes.exc.QubesVMNotHaltedError(self.dest)
|
|
|
|
requested_size = None
|
|
if untrusted_payload:
|
|
try:
|
|
untrusted_value = int(untrusted_payload.decode('ascii'))
|
|
except (UnicodeDecodeError, ValueError):
|
|
raise qubes.api.ProtocolError('Invalid value')
|
|
self.enforce(untrusted_value > 0)
|
|
requested_size = untrusted_value
|
|
del untrusted_value
|
|
del untrusted_payload
|
|
|
|
path = yield from self.dest.storage.import_data(
|
|
self.arg, requested_size)
|
|
self.enforce(' ' not in path)
|
|
if requested_size is None:
|
|
size = self.dest.volumes[self.arg].size
|
|
else:
|
|
size = requested_size
|
|
|
|
# when we know the action is allowed, inform extensions that it will
|
|
# be performed
|
|
self.dest.fire_event(
|
|
'domain-volume-import-begin', volume=self.arg, size=size)
|
|
|
|
return '{} {}'.format(size, path)
|
|
|
|
@qubes.api.method('internal.vm.volume.ImportEnd')
|
|
@asyncio.coroutine
|
|
def vm_volume_import_end(self, untrusted_payload):
|
|
'''
|
|
This is second half of admin.vm.volume.Import handling. It is called
|
|
when actual import is finished. Response from this method is sent do
|
|
the client (as a response for admin.vm.volume.Import call).
|
|
'''
|
|
self.enforce(self.arg in self.dest.volumes.keys())
|
|
success = untrusted_payload == b'ok'
|
|
|
|
try:
|
|
yield from self.dest.storage.import_data_end(self.arg,
|
|
success=success)
|
|
except:
|
|
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
|
|
success=False)
|
|
raise
|
|
|
|
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
|
|
success=success)
|
|
|
|
if not success:
|
|
raise qubes.exc.QubesException('Data import failed')
|
|
|
|
@qubes.api.method('internal.SuspendPre', no_payload=True)
|
|
@asyncio.coroutine
|
|
def suspend_pre(self):
|
|
'''
|
|
Method called before host system goes to sleep.
|
|
|
|
:return:
|
|
'''
|
|
|
|
# first notify all VMs
|
|
processes = []
|
|
for vm in self.app.domains:
|
|
if isinstance(vm, qubes.vm.adminvm.AdminVM):
|
|
continue
|
|
if not vm.is_running():
|
|
continue
|
|
if not vm.features.check_with_template('qrexec', False):
|
|
continue
|
|
try:
|
|
proc = yield from vm.run_service(
|
|
'qubes.SuspendPreAll', user='root',
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
processes.append(proc)
|
|
except qubes.exc.QubesException as e:
|
|
vm.log.warning('Failed to run qubes.SuspendPreAll: %s', str(e))
|
|
|
|
# FIXME: some timeout?
|
|
if processes:
|
|
yield from asyncio.wait([p.wait() for p in processes])
|
|
|
|
coros = []
|
|
# then suspend/pause VMs
|
|
for vm in self.app.domains:
|
|
if isinstance(vm, qubes.vm.adminvm.AdminVM):
|
|
continue
|
|
if vm.is_running():
|
|
coros.append(vm.suspend())
|
|
if coros:
|
|
yield from asyncio.wait(coros)
|
|
|
|
@qubes.api.method('internal.SuspendPost', no_payload=True)
|
|
@asyncio.coroutine
|
|
def suspend_post(self):
|
|
'''
|
|
Method called after host system wake up from sleep.
|
|
|
|
:return:
|
|
'''
|
|
|
|
coros = []
|
|
# first resume/unpause VMs
|
|
for vm in self.app.domains:
|
|
if isinstance(vm, qubes.vm.adminvm.AdminVM):
|
|
continue
|
|
if vm.get_power_state() in ["Paused", "Suspended"]:
|
|
coros.append(vm.resume())
|
|
if coros:
|
|
yield from asyncio.wait(coros)
|
|
|
|
# then notify all VMs
|
|
processes = []
|
|
for vm in self.app.domains:
|
|
if isinstance(vm, qubes.vm.adminvm.AdminVM):
|
|
continue
|
|
if not vm.is_running():
|
|
continue
|
|
if not vm.features.check_with_template('qrexec', False):
|
|
continue
|
|
try:
|
|
proc = yield from vm.run_service(
|
|
'qubes.SuspendPostAll', user='root',
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
processes.append(proc)
|
|
except qubes.exc.QubesException as e:
|
|
vm.log.warning('Failed to run qubes.SuspendPostAll: %s', str(e))
|
|
|
|
# FIXME: some timeout?
|
|
if processes:
|
|
yield from asyncio.wait([p.wait() for p in processes])
|