diff --git a/Makefile b/Makefile index 0b06cc8f..29ca0f24 100644 --- a/Makefile +++ b/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; \ diff --git a/qubes-rpc/admin.vm.volume.Import b/qubes-rpc/admin.vm.volume.Import new file mode 100755 index 00000000..afde7875 --- /dev/null +++ b/qubes-rpc/admin.vm.volume.Import @@ -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" diff --git a/qubes/api/admin.py b/qubes/api/admin.py index e58bc928..712b75a7 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -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): diff --git a/qubes/api/internal.py b/qubes/api/internal.py index 68cad53c..b9bad5dd 100644 --- a/qubes/api/internal.py +++ b/qubes/api/internal.py @@ -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') diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 2d818114..ac56ba8a 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -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")