Эх сурвалжийг харах

storage: add volume clone method

Clone volume without retrieving all the data.

QubesOS/qubes-issues#2622
Marek Marczykowski-Górecki 7 жил өмнө
parent
commit
998a42703f

+ 25 - 0
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

+ 39 - 1
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):