From 5c7b57e6909ca2c92d50492230d4b606e5e09a1a Mon Sep 17 00:00:00 2001 From: WillyPillow Date: Tue, 14 Jul 2020 01:44:36 +0800 Subject: [PATCH 1/3] Add admin.vm.volume.Clear call (QubesOS/qubes-issues#5946) --- qubes/api/admin.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 60417b18..c6656e7a 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 volume.import_data(size) + self.dest.fire_event('domain-volume-import-begin', + volume=self.arg, size=size) + pathlib.Path(path).touch() + try: + yield from volume.import_data_end(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 From 56fbf108f8afb511820352219107a74acfa056a0 Mon Sep 17 00:00:00 2001 From: WillyPillow Date: Thu, 16 Jul 2020 00:38:02 +0800 Subject: [PATCH 2/3] Use self.dest.storage.import* wrappers instead. --- qubes/api/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qubes/api/admin.py b/qubes/api/admin.py index c6656e7a..2c9c30d8 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -499,12 +499,12 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): size = volume.size # Clear the volume by importing empty data into it - path = yield from volume.import_data(size) + 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 volume.import_data_end(True) + 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) From be69d8ddb7ce2d493211599b6519d447c176cf52 Mon Sep 17 00:00:00 2001 From: WillyPillow Date: Thu, 16 Jul 2020 00:39:16 +0800 Subject: [PATCH 3/3] Add tests for vm.volume.Clear. --- qubes/tests/api_admin.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) 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',