admin: implement admin.vm.volume.Import
Implement this in two parts: 1. Permissions checks, getting a path from appropriate storage pool 2. Actual data import The first part is done by qubesd in a standard way, but then, instead of accepting all the data (which may be several GB), return a path to which a shell script (in practice: `dd` command) will write the data. Then the script call back to qubesd again to report success/failure and qubesd response from that call is actually returned to the user. This way we do not pass all the data through qubesd, but still can control the process from there in a meaningful way. Note that the last part (second call to qubesd) may perform all kind of verification (like a signature check on the data, or so) and can also prevent VM from starting (hooking also domain-pre-start event) from not verified image. QubesOS/qubes-issues#2622
This commit is contained in:
parent
46b60dbf42
commit
3cacf290bb
1
Makefile
1
Makefile
@ -176,6 +176,7 @@ endif
|
||||
ln -s ../../usr/libexec/qubes/qubesd-query-fast \
|
||||
$(DESTDIR)/etc/qubes-rpc/$$method; \
|
||||
done
|
||||
install qubes-rpc/admin.vm.volume.Import $(DESTDIR)/etc/qubes-rpc/
|
||||
for method in $(ADMIN_API_METHODS); do \
|
||||
install -m 0644 qubes-rpc-policy/admin-default \
|
||||
$(DESTDIR)/etc/qubes-rpc/policy/$$method; \
|
||||
|
35
qubes-rpc/admin.vm.volume.Import
Executable file
35
qubes-rpc/admin.vm.volume.Import
Executable file
@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# use temporary file, because env variables deal poorly with \0 inside
|
||||
tmpfile=$(mktemp)
|
||||
trap "rm -f $tmpfile" EXIT
|
||||
qubesd-query -e \
|
||||
"$QREXEC_REMOTE_DOMAIN" \
|
||||
"admin.vm.volume.Import" \
|
||||
"$QREXEC_REQUESTED_TARGET" \
|
||||
"$1" >$tmpfile
|
||||
|
||||
# exit if qubesd returned an error (not '0\0')
|
||||
if [ "$(head -c 2 $tmpfile | xxd -p)" != "3000" ]; then
|
||||
cat "$tmpfile"
|
||||
exit 1
|
||||
fi
|
||||
size=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 1)
|
||||
path=$(tail -c +3 "$tmpfile"|cut -d ' ' -f 2)
|
||||
|
||||
# now process stdin into this path
|
||||
if dd bs=4k of="$path" count="$size" iflag=count_bytes \
|
||||
conv=sparse,notrunc,nocreat,fdatasync; then
|
||||
status="ok"
|
||||
else
|
||||
status="fail"
|
||||
fi
|
||||
|
||||
# send status notification to qubesd, and pass its response to the caller
|
||||
echo -n "$status" | qubesd-query -c /var/run/qubesd.internal.sock \
|
||||
"$QREXEC_REMOTE_DOMAIN" \
|
||||
"internal.vm.volume.ImportEnd" \
|
||||
"$QREXEC_REQUESTED_TARGET" \
|
||||
"$1"
|
@ -307,6 +307,35 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
|
||||
self.dest.storage.resize(self.arg, size)
|
||||
self.app.save()
|
||||
|
||||
@qubes.api.method('admin.vm.volume.Import', no_payload=True)
|
||||
@asyncio.coroutine
|
||||
def vm_volume_import(self):
|
||||
'''Import volume data.
|
||||
|
||||
Note that this function only returns a path to where data should be
|
||||
written, actual importing is done by a script in /etc/qubes-rpc
|
||||
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.
|
||||
'''
|
||||
assert self.arg in self.dest.volumes.keys()
|
||||
|
||||
self.fire_event_for_permission()
|
||||
|
||||
if not self.dest.is_halted():
|
||||
raise qubes.exc.QubesVMNotHaltedError(self.dest)
|
||||
|
||||
path = self.dest.storage.import_data(self.arg)
|
||||
assert ' ' not in path
|
||||
size = self.dest.volumes[self.arg].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)
|
||||
|
||||
return '{} {}'.format(size, path)
|
||||
|
||||
@qubes.api.method('admin.pool.List', no_payload=True)
|
||||
@asyncio.coroutine
|
||||
def pool_list(self):
|
||||
|
@ -83,3 +83,27 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
|
||||
|
||||
# TODO convert to coroutine
|
||||
self.dest.cleanup()
|
||||
|
||||
@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).
|
||||
'''
|
||||
assert self.arg in self.dest.volumes.keys()
|
||||
success = untrusted_payload == b'ok'
|
||||
|
||||
try:
|
||||
self.dest.storage.import_data_end(self.arg, success=success)
|
||||
except:
|
||||
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
|
||||
succeess=False)
|
||||
raise
|
||||
|
||||
self.dest.fire_event('domain-volume-import-end', volume=self.arg,
|
||||
succeess=success)
|
||||
|
||||
if not success:
|
||||
raise qubes.exc.QubesException('Data import failed')
|
||||
|
@ -553,6 +553,18 @@ class Storage(object):
|
||||
|
||||
return self.pools[volume].import_data(self.vm.volumes[volume])
|
||||
|
||||
def import_data_end(self, volume, success):
|
||||
''' Helper function to finish/cleanup data import
|
||||
(pool.import_data_end( volume))'''
|
||||
assert isinstance(volume, (Volume, str)), \
|
||||
"You need to pass a Volume or pool name as str"
|
||||
if isinstance(volume, Volume):
|
||||
return self.pools[volume.name].import_data_end(volume,
|
||||
success=success)
|
||||
|
||||
return self.pools[volume].import_data_end(self.vm.volumes[volume],
|
||||
success=success)
|
||||
|
||||
|
||||
class Pool(object):
|
||||
''' A Pool is used to manage different kind of volumes (File
|
||||
@ -624,14 +636,18 @@ class Pool(object):
|
||||
raise self._not_implemented("export")
|
||||
|
||||
def import_data(self, volume):
|
||||
''' Returns an object that can be `open()`.
|
||||
|
||||
Storage implementation may register for
|
||||
`domain-volume-import-end` event to cleanup after this. The
|
||||
event will have also success=True|False information.
|
||||
'''
|
||||
''' Returns an object that can be `open()`. '''
|
||||
raise self._not_implemented("import")
|
||||
|
||||
def import_data_end(self, volume, success):
|
||||
''' End data import operation. This may be used by pool
|
||||
implementation to commit changes, cleanup temporary files etc.
|
||||
|
||||
:param success: True if data import was successful, otherwise False
|
||||
'''
|
||||
# by default do nothing
|
||||
pass
|
||||
|
||||
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
|
||||
''' Imports data to a volume in this pool '''
|
||||
raise self._not_implemented("import_volume")
|
||||
|
Loading…
Reference in New Issue
Block a user