diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 60417b18..2c9c30d8 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -27,6 +27,7 @@ import itertools import os import string import subprocess +import pathlib import libvirt import lxml.etree @@ -486,6 +487,32 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): finally: # even if calling qubes.ResizeDisk inside the VM failed self.app.save() + @qubes.api.method('admin.vm.volume.Clear', no_payload=True, + scope='local', write=True) + @asyncio.coroutine + def vm_volume_clear(self): + self.enforce(self.arg in self.dest.volumes.keys()) + + self.fire_event_for_permission() + + volume = self.dest.volumes[self.arg] + size = volume.size + + # Clear the volume by importing empty data into it + path = yield from self.dest.storage.import_data(self.arg, size) + self.dest.fire_event('domain-volume-import-begin', + volume=self.arg, size=size) + pathlib.Path(path).touch() + try: + yield from self.dest.storage.import_data_end(self.arg, True) + 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=True) + self.app.save() + @qubes.api.method('admin.vm.volume.Set.revisions_to_keep', scope='local', write=True) @asyncio.coroutine diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 6623ba73..be94d916 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2756,6 +2756,44 @@ netvm default=True type=vm \n''' } value = self.call_mgmt_func(b'admin.pool.volume.List', b'dom0', b'pool1') self.assertEqual(value, 'vol1\nvol2\n') + + def test_710_vm_volume_clear(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpfile = os.path.join(tmpdir, 'testfile') + + @asyncio.coroutine + def coroutine_mock(*args, **kwargs): + return tmpfile + + self.vm.volumes = unittest.mock.MagicMock() + volumes_conf = { + 'keys.return_value': ['root', 'private', 'volatile', 'kernel'], + '__getitem__.return_value.size': 0xdeadbeef + } + self.vm.volumes.configure_mock(**volumes_conf) + self.vm.storage = unittest.mock.Mock() + storage_conf = { + 'import_data.side_effect': coroutine_mock, + 'import_data_end.side_effect': self.dummy_coro + } + self.vm.storage.configure_mock(**storage_conf) + self.app.domains['test-vm1'].fire_event = self.emitter.fire_event + value = self.call_mgmt_func(b'admin.vm.volume.Clear', + b'test-vm1', b'private') + self.assertIsNone(value) + self.assertTrue(os.path.exists(tmpfile)) + self.assertEqual(self.vm.volumes.mock_calls, [ + unittest.mock.call.keys(), + unittest.mock.call.__getattr__('__getitem__')('private')]) + self.assertEqual(self.vm.storage.mock_calls, [ + unittest.mock.call.import_data('private', 0xdeadbeef), + unittest.mock.call.import_data_end('private', True)]) + self.assertEventFired( + self.emitter, 'admin-permission:admin.vm.volume.Clear') + self.assertEventFired( + self.emitter, 'domain-volume-import-begin') + self.assertEventFired( + self.emitter, 'domain-volume-import-end') def test_800_current_state_default(self): value = self.call_mgmt_func(b'admin.vm.CurrentState', b'test-vm1') @@ -3057,6 +3095,7 @@ netvm default=True type=vm \n''' b'admin.vm.volume.Info', b'admin.vm.volume.Revert', b'admin.vm.volume.Resize', + b'admin.vm.volume.Clear', b'admin.vm.Start', b'admin.vm.Shutdown', b'admin.vm.Pause',