From 998a42703f379da5df9fb6588330a4c2e61e6ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 19 Jun 2017 03:02:43 +0200 Subject: [PATCH] storage: add volume clone method Clone volume without retrieving all the data. QubesOS/qubes-issues#2622 --- qubesadmin/storage.py | 25 +++++++++++++++++++++++ qubesadmin/tests/storage.py | 40 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/qubesadmin/storage.py b/qubesadmin/storage.py index 21614a9..fb32fd2 100644 --- a/qubesadmin/storage.py +++ b/qubesadmin/storage.py @@ -93,6 +93,11 @@ class Volume(object): return self.pool == other.pool and self.vid == other.vid return NotImplemented + @property + def name(self): + '''per-VM volume name, if available''' + return self._vm_name + @property def pool(self): '''Storage volume pool name.''' @@ -195,6 +200,26 @@ class Volume(object): ''' self._qubesd_call('Import', payload_stream=stream) + def clone(self, source): + ''' Clone data from sane volume of another VM. + + This function override existing volume content. + This operation is implemented for VM volumes - those in vm.volumes + collection (not pool.volumes). + + :param source: source volume object + ''' + + # pylint: disable=protected-access + if source._vm_name is None or self._vm_name is None: + raise NotImplementedError( + 'Operation implemented only for VM volumes') + if source._vm_name != self._vm_name: + raise ValueError('Source and target volume must have the same type') + + # this call is to *source* volume, because we extract data from there + source._qubesd_call('Clone', payload=str(self._vm).encode()) + class Pool(object): ''' A Pool is used to manage different kind of volumes (File diff --git a/qubesadmin/tests/storage.py b/qubesadmin/tests/storage.py index b0cee76..76d3134 100644 --- a/qubesadmin/tests/storage.py +++ b/qubesadmin/tests/storage.py @@ -161,6 +161,30 @@ class TestVMVolume(qubesadmin.tests.QubesTestCase): input_proc.stdout.close() self.assertAllCalled() + def test_050_clone(self): + self.app.expected_calls[ + ('source-vm', 'admin.vm.volume.Clone', 'volname', b'test-vm')] = \ + b'0\x00' + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00source-vm class=AppVM state=Halted\n' + self.app.expected_calls[ + ('source-vm', 'admin.vm.volume.List', None, None)] = \ + b'0\x00volname\nother\n' + self.vol.clone(self.app.domains['source-vm'].volumes['volname']) + self.assertAllCalled() + + def test_050_clone_wrong_volume(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00source-vm class=AppVM state=Halted\n' + self.app.expected_calls[ + ('source-vm', 'admin.vm.volume.List', None, None)] = \ + b'0\x00volname\nother\n' + with self.assertRaises(ValueError): + self.vol.clone(self.app.domains['source-vm'].volumes['other']) + self.assertAllCalled() + class TestPoolVolume(TestVMVolume): def setUp(self): @@ -245,7 +269,21 @@ class TestPoolVolume(TestVMVolume): self.assertAllCalled() def test_040_import_data(self): - self.skipTest('admin.pool.vm.Import not supported') + self.skipTest('admin.pool.volume.Import not supported') + + def test_050_clone(self): + self.app.expected_calls[ + ('dom0', 'admin.vm.List', None, None)] = \ + b'0\x00source-vm class=AppVM state=Halted\n' + self.app.expected_calls[ + ('source-vm', 'admin.vm.volume.List', None, None)] = \ + b'0\x00volname\nother\n' + with self.assertRaises(NotImplementedError): + self.vol.clone(self.app.domains['source-vm'].volumes['volname']) + self.assertAllCalled() + + def test_050_clone_wrong_volume(self): + self.skipTest('admin.pool.volume.Clone not supported') class TestPool(qubesadmin.tests.QubesTestCase):