Skip to content

Commit

Permalink
storage: add volume clone method
Browse files Browse the repository at this point in the history
Clone volume without retrieving all the data.

QubesOS/qubes-issues#2622
  • Loading branch information
marmarek committed Jun 19, 2017
1 parent 90141a1 commit 998a427
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 1 deletion.
25 changes: 25 additions & 0 deletions qubesadmin/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ def __eq__(self, other):
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.'''
Expand Down Expand Up @@ -195,6 +200,26 @@ def import_data(self, stream):
'''
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
Expand Down
40 changes: 39 additions & 1 deletion qubesadmin/tests/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,30 @@ def test_040_import_data(self):
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):
Expand Down Expand Up @@ -245,7 +269,21 @@ def test_031_revert(self):
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):
Expand Down

0 comments on commit 998a427

Please sign in to comment.