Merge branch 'two-stage-clone'

This commit is contained in:
Marek Marczykowski-Górecki 2017-07-04 03:37:41 +02:00
commit f83c516082
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
8 changed files with 245 additions and 244 deletions

View File

@ -35,7 +35,6 @@ ADMIN_API_METHODS_SIMPLE = \
admin.property.List \ admin.property.List \
admin.property.Reset \ admin.property.Reset \
admin.property.Set \ admin.property.Set \
admin.vm.Clone \
admin.vm.Create.AppVM \ admin.vm.Create.AppVM \
admin.vm.Create.DispVM \ admin.vm.Create.DispVM \
admin.vm.Create.StandaloneVM \ admin.vm.Create.StandaloneVM \

View File

@ -293,22 +293,57 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.dest.storage.get_pool(volume).revert(revision) self.dest.storage.get_pool(volume).revert(revision)
self.app.save() self.app.save()
@qubes.api.method('admin.vm.volume.Clone') @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True)
@asyncio.coroutine @asyncio.coroutine
def vm_volume_clone(self, untrusted_payload): def vm_volume_clone_from(self):
assert self.arg in self.dest.volumes.keys() assert self.arg in self.dest.volumes.keys()
untrusted_target = untrusted_payload.decode('ascii').strip()
del untrusted_payload
qubes.vm.validate_name(None, None, untrusted_target)
target_vm = self.app.domains[untrusted_target]
del untrusted_target
assert self.arg in target_vm.volumes.keys()
volume = self.dest.volumes[self.arg] volume = self.dest.volumes[self.arg]
self.fire_event_for_permission(target_vm=target_vm, volume=volume) self.fire_event_for_permission(volume=volume)
yield from target_vm.storage.clone_volume(self.dest, self.arg) token = qubes.utils.random_string(32)
# save token on self.app, as self is not persistent
if not hasattr(self.app, 'api_admin_pending_clone'):
self.app.api_admin_pending_clone = {}
# don't handle collisions any better - if someone is so much out of
# luck, can try again anyway
assert token not in self.app.api_admin_pending_clone
self.app.api_admin_pending_clone[token] = volume
return token
@qubes.api.method('admin.vm.volume.CloneTo')
@asyncio.coroutine
def vm_volume_clone_to(self, untrusted_payload):
assert self.arg in self.dest.volumes.keys()
untrusted_token = untrusted_payload.decode('ascii').strip()
del untrusted_payload
assert untrusted_token in getattr(self.app,
'api_admin_pending_clone', {})
token = untrusted_token
del untrusted_token
src_volume = self.app.api_admin_pending_clone[token]
del self.app.api_admin_pending_clone[token]
# make sure the volume still exists, but invalidate token anyway
assert str(src_volume.pool) in self.app.pools
assert src_volume in self.app.pools[str(src_volume.pool)].volumes
dst_volume = self.dest.volumes[self.arg]
self.fire_event_for_permission(src_volume=src_volume,
dst_volume=dst_volume)
op_retval = dst_volume.import_volume(src_volume)
# clone/import functions may be either synchronous or asynchronous
# in the later case, we need to wait for them to finish
if asyncio.iscoroutine(op_retval):
op_retval = yield from op_retval
self.dest.volumes[self.arg] = op_retval
self.app.save() self.app.save()
@qubes.api.method('admin.vm.volume.Resize') @qubes.api.method('admin.vm.volume.Resize')
@ -829,40 +864,6 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.app.save() self.app.save()
@qubes.api.method('admin.vm.Clone')
@asyncio.coroutine
def vm_clone(self, untrusted_payload):
assert not self.arg
assert untrusted_payload.startswith(b'name=')
untrusted_name = untrusted_payload[5:].decode('ascii')
qubes.vm.validate_name(None, None, untrusted_name)
new_name = untrusted_name
del untrusted_payload
if new_name in self.app.domains:
raise qubes.exc.QubesValueError('Already exists')
self.fire_event_for_permission(new_name=new_name)
src_vm = self.dest
dst_vm = self.app.add_new_vm(src_vm.__class__, name=new_name)
try:
dst_vm.clone_properties(src_vm)
dst_vm.tags.update(src_vm.tags)
dst_vm.features.update(src_vm.features)
dst_vm.firewall.clone(src_vm.firewall)
for devclass in src_vm.devices:
for device_assignment in src_vm.devices[devclass].assignments():
dst_vm.devices[devclass].attach(device_assignment.clone())
yield from dst_vm.clone_disk_files(src_vm)
except:
del self.app.domains[dst_vm]
raise
self.app.save()
@qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
for ep in pkg_resources.iter_entry_points('qubes.devices')), for ep in pkg_resources.iter_entry_points('qubes.devices')),
no_payload=True) no_payload=True)

View File

@ -146,6 +146,12 @@ class Volume(object):
''' '''
raise self._not_implemented("create") raise self._not_implemented("create")
def remove(self):
''' Remove volume.
This can be implemented as a coroutine.'''
raise self._not_implemented("remove")
def commit(self): def commit(self):
''' Write the snapshot to disk ''' Write the snapshot to disk
@ -171,7 +177,11 @@ class Volume(object):
def import_volume(self, src_volume): def import_volume(self, src_volume):
''' Imports data from a different volume (possibly in a different ''' Imports data from a different volume (possibly in a different
pool ''' pool.
The needs to be create()d first.
This can be implemented as a coroutine. '''
# pylint: disable=unused-argument # pylint: disable=unused-argument
raise self._not_implemented("import_volume") raise self._not_implemented("import_volume")
@ -442,16 +452,9 @@ class Storage(object):
dst_pool = self.vm.app.get_pool(config['pool']) dst_pool = self.vm.app.get_pool(config['pool'])
dst = dst_pool.init_volume(self.vm, config) dst = dst_pool.init_volume(self.vm, config)
src_volume = src_vm.volumes[name] src_volume = src_vm.volumes[name]
src_pool = src_volume.pool msg = "Importing volume {!s} from vm {!s}"
if dst_pool == src_pool: self.vm.log.info(msg.format(src_volume.name, src_vm.name))
msg = "Cloning volume {!s} from vm {!s}" clone_op_ret = dst.import_volume(src_volume)
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
clone_op_ret = dst_pool.clone(src_volume, dst)
else:
msg = "Importing volume {!s} from vm {!s}"
self.vm.log.info(msg.format(src_volume.name, src_vm.name))
clone_op_ret = dst_pool.import_volume(
dst_pool, dst, src_pool, src_volume)
# clone/import functions may be either synchronous or asynchronous # clone/import functions may be either synchronous or asynchronous
# in the later case, we need to wait for them to finish # in the later case, we need to wait for them to finish
@ -520,7 +523,7 @@ class Storage(object):
for name, volume in self.vm.volumes.items(): for name, volume in self.vm.volumes.items():
self.log.info('Removing volume %s: %s' % (name, volume.vid)) self.log.info('Removing volume %s: %s' % (name, volume.vid))
try: try:
ret = volume.pool.remove(volume) ret = volume.remove()
if asyncio.iscoroutine(ret): if asyncio.iscoroutine(ret):
futures.append(ret) futures.append(ret)
except (IOError, OSError) as e: except (IOError, OSError) as e:
@ -614,6 +617,58 @@ class Storage(object):
return self.vm.volumes[volume].import_data_end(success=success) return self.vm.volumes[volume].import_data_end(success=success)
class VolumesCollection(object):
'''Convenient collection wrapper for pool.get_volume and
pool.list_volumes
'''
def __init__(self, pool):
self._pool = pool
def __getitem__(self, item):
''' Get a single volume with given Volume ID.
You can also a Volume instance to get the same Volume or KeyError if
Volume no longer exists.
:param item: a Volume ID (str) or a Volume instance
'''
if isinstance(item, Volume):
if item.pool == self._pool:
return self[item.vid]
else:
raise KeyError(item)
try:
return self._pool.get_volume(item)
except NotImplementedError:
for vol in self:
if vol.vid == item:
return vol
# if list_volumes is not implemented too, it will raise
# NotImplementedError again earlier
raise KeyError(item)
def __iter__(self):
''' Get iterator over pool's volumes '''
return iter(self._pool.list_volumes())
def __contains__(self, item):
''' Check if given volume (either Volume ID or Volume instance) is
present in the pool
'''
try:
return self[item] is not None
except KeyError:
return False
def keys(self):
''' Return list of volume IDs '''
return [vol.vid for vol in self]
def values(self):
''' Return list of Volumes'''
return [vol for vol in self]
class Pool(object): class Pool(object):
''' A Pool is used to manage different kind of volumes (File ''' A Pool is used to manage different kind of volumes (File
based/LVM/Btrfs/...). based/LVM/Btrfs/...).
@ -626,6 +681,7 @@ class Pool(object):
def __init__(self, name, revisions_to_keep=1, **kwargs): def __init__(self, name, revisions_to_keep=1, **kwargs):
super(Pool, self).__init__(**kwargs) super(Pool, self).__init__(**kwargs)
self._volumes_collection = VolumesCollection(self)
self.name = name self.name = name
self.revisions_to_keep = revisions_to_keep self.revisions_to_keep = revisions_to_keep
kwargs['name'] = self.name kwargs['name'] = self.name
@ -666,12 +722,6 @@ class Pool(object):
''' '''
raise self._not_implemented("init_volume") raise self._not_implemented("init_volume")
def remove(self, volume):
''' Remove volume.
This can be implemented as a coroutine.'''
raise self._not_implemented("remove")
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
''' Called when the domain changes its name ''' ''' Called when the domain changes its name '''
raise self._not_implemented("rename") raise self._not_implemented("rename")
@ -684,8 +734,19 @@ class Pool(object):
@property @property
def volumes(self): def volumes(self):
''' Return a collection of volumes managed by this pool '''
return self._volumes_collection
def list_volumes(self):
''' Return a list of volumes managed by this pool ''' ''' Return a list of volumes managed by this pool '''
raise self._not_implemented("volumes") raise self._not_implemented("list_volumes")
def get_volume(self, vid):
''' Return a volume with *vid* from this pool
:raise KeyError: if no volume is found
'''
raise self._not_implemented("get_volume")
def _not_implemented(self, method_name): def _not_implemented(self, method_name):
''' Helper for emitting helpful `NotImplementedError` exceptions ''' ''' Helper for emitting helpful `NotImplementedError` exceptions '''
@ -740,8 +801,7 @@ class VmCreationManager(object):
if type is not None and value is not None and tb is not None: if type is not None and value is not None and tb is not None:
for volume in self.vm.volumes.values(): for volume in self.vm.volumes.values():
try: try:
pool = volume.pool volume.remove()
pool.remove(volume)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass
os.rmdir(self.vm.dir_path) os.rmdir(self.vm.dir_path)

View File

@ -79,16 +79,6 @@ class FilePool(qubes.storage.Pool):
self._volumes += [volume] self._volumes += [volume]
return volume return volume
def remove(self, volume):
if not volume.internal:
return # do not remove random attached file volumes
elif volume._is_snapshot:
return # no need to remove, because it's just a snapshot
else:
_remove_if_exists(volume.path)
if volume._is_origin:
_remove_if_exists(volume.path_cow)
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
assert issubclass(volume.__class__, FileVolume) assert issubclass(volume.__class__, FileVolume)
subdir, _, volume_path = volume.vid.split('/', 2) subdir, _, volume_path = volume.vid.split('/', 2)
@ -152,8 +142,7 @@ class FilePool(qubes.storage.Pool):
return os.path.join(self.dir_path, self._vid_prefix(vm)) return os.path.join(self.dir_path, self._vid_prefix(vm))
@property def list_volumes(self):
def volumes(self):
return self._volumes return self._volumes
@ -202,6 +191,16 @@ class FileVolume(qubes.storage.Volume):
else: else:
create_sparse_file(self.path, self.size) create_sparse_file(self.path, self.size)
def remove(self):
if not self.internal:
return # do not remove random attached file volumes
elif self._is_snapshot:
return # no need to remove, because it's just a snapshot
else:
_remove_if_exists(self.path)
if self._is_origin:
_remove_if_exists(self.path_cow)
def is_dirty(self): def is_dirty(self):
return False # TODO: How to implement this? return False # TODO: How to implement this?
@ -262,6 +261,7 @@ class FileVolume(qubes.storage.Volume):
msg = msg.format(src_volume, self) msg = msg.format(src_volume, self)
assert not src_volume.snap_on_start, msg assert not src_volume.snap_on_start, msg
if self.save_on_stop: if self.save_on_stop:
_remove_if_exists(self.path)
copy_file(src_volume.export(), self.path) copy_file(src_volume.export(), self.path)
return self return self

View File

@ -93,6 +93,9 @@ class LinuxModules(Volume):
def create(self): def create(self):
return self return self
def remove(self):
pass
def commit(self): def commit(self):
return self return self
@ -154,9 +157,6 @@ class LinuxKernel(Pool):
def import_volume(self, dst_pool, dst_volume, src_pool, src_volume): def import_volume(self, dst_pool, dst_volume, src_pool, src_volume):
pass pass
def remove(self, volume):
pass
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
return volume return volume

View File

@ -88,18 +88,6 @@ class ThinPool(qubes.storage.Pool):
volume_config['pool'] = self volume_config['pool'] = self
return ThinVolume(**volume_config) return ThinVolume(**volume_config)
def remove(self, volume):
assert volume.vid
if volume.is_dirty():
cmd = ['remove', volume._vid_snap]
qubes_lvm(cmd, self.log)
if not os.path.exists(volume.path):
return
cmd = ['remove', volume.vid]
qubes_lvm(cmd, self.log)
reset_cache()
def rename(self, volume, old_name, new_name): def rename(self, volume, old_name, new_name):
''' Called when the domain changes its name ''' ''' Called when the domain changes its name '''
new_vid = "{!s}/vm-{!s}-{!s}".format(self.volume_group, new_name, new_vid = "{!s}/vm-{!s}-{!s}".format(self.volume_group, new_name,
@ -120,8 +108,7 @@ class ThinPool(qubes.storage.Pool):
def setup(self): def setup(self):
pass # TODO Should we create a non existing pool? pass # TODO Should we create a non existing pool?
@property def list_volumes(self):
def volumes(self):
''' Return a list of volumes managed by this pool ''' ''' Return a list of volumes managed by this pool '''
volumes = [] volumes = []
for vid, vol_info in size_cache.items(): for vid, vol_info in size_cache.items():
@ -133,7 +120,7 @@ class ThinPool(qubes.storage.Pool):
# implementation detail volume # implementation detail volume
continue continue
config = { config = {
'pool': self.name, 'pool': self,
'vid': vid, 'vid': vid,
'name': vid, 'name': vid,
'volume_group': self.volume_group, 'volume_group': self.volume_group,
@ -297,6 +284,18 @@ class ThinVolume(qubes.storage.Volume):
reset_cache() reset_cache()
return self return self
def remove(self):
assert self.vid
if self.is_dirty():
cmd = ['remove', self._vid_snap]
qubes_lvm(cmd, self.log)
if not os.path.exists(self.path):
return
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
reset_cache()
def export(self): def export(self):
''' Returns an object that can be `open()`. ''' ''' Returns an object that can be `open()`. '''
devpath = '/dev/' + self.vid devpath = '/dev/' + self.vid
@ -306,18 +305,17 @@ class ThinVolume(qubes.storage.Volume):
if not src_volume.save_on_stop: if not src_volume.save_on_stop:
return self return self
src_path = src_volume.export()
# HACK: neat trick to speed up testing if you have same physical thin # HACK: neat trick to speed up testing if you have same physical thin
# pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm # pool assigned to two qubes-pools i.e: qubes_dom0 and test-lvm
# pylint: disable=line-too-long # pylint: disable=line-too-long
if isinstance(src_volume.pool, ThinPool) and \ if isinstance(src_volume.pool, ThinPool) and \
src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA src_volume.pool.thin_pool == self.pool.thin_pool: # NOQA
cmd = ['remove', self.vid]
qubes_lvm(cmd, self.log)
cmd = ['clone', str(src_volume), str(self)] cmd = ['clone', str(src_volume), str(self)]
qubes_lvm(cmd, self.log) qubes_lvm(cmd, self.log)
else: else:
self.create() src_path = src_volume.export()
cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid, cmd = ['sudo', 'dd', 'if=' + src_path, 'of=/dev/' + self.vid,
'conv=sparse'] 'conv=sparse']
subprocess.check_call(cmd) subprocess.check_call(cmd)

View File

@ -21,6 +21,7 @@
''' Tests for management calls endpoints ''' ''' Tests for management calls endpoints '''
import asyncio import asyncio
import operator
import os import os
import shutil import shutil
import unittest.mock import unittest.mock
@ -31,6 +32,7 @@ import qubes
import qubes.devices import qubes.devices
import qubes.api.admin import qubes.api.admin
import qubes.tests import qubes.tests
import qubes.storage
# properties defined in API # properties defined in API
volume_properties = [ volume_properties = [
@ -1236,72 +1238,6 @@ class TC_00_VMs(AdminAPITestCase):
self.assertNotIn('test-vm2', self.app.domains) self.assertNotIn('test-vm2', self.app.domains)
self.assertFalse(self.app.save.called) self.assertFalse(self.app.save.called)
@unittest.mock.patch('qubes.storage.Storage.clone')
@unittest.mock.patch('qubes.storage.Storage.verify')
def test_350_vm_clone(self, mock_verify, mock_clone):
mock_clone.side_effect = self.dummy_coro
mock_verify.side_effect = self.dummy_coro
self.call_mgmt_func(b'admin.vm.Clone',
b'test-vm1', b'', b'name=test-vm2')
self.assertIn('test-vm2', self.app.domains)
vm = self.app.domains['test-vm2']
self.assertEqual(vm.label, self.app.get_label('red'))
self.assertEqual(vm.template, self.app.domains['test-template'])
self.assertEqual(vm.tags, self.vm.tags)
self.assertEqual(vm.features, self.vm.features)
self.assertEqual(vm.firewall, self.vm.firewall)
self.assertEqual(mock_clone.mock_calls,
[unittest.mock.call(self.app.domains['test-vm2']).clone(
self.app.domains['test-vm1'])])
self.assertTrue(os.path.exists(os.path.join(
self.test_base_dir, 'appvms', 'test-vm2')))
self.assertTrue(self.app.save.called)
@unittest.mock.patch('qubes.storage.Storage.clone')
@unittest.mock.patch('qubes.storage.Storage.verify')
def test_351_vm_clone_extra_params(self, mock_verify, mock_clone):
mock_clone.side_effect = self.dummy_coro
mock_verify.side_effect = self.dummy_coro
with self.assertRaises(qubes.exc.QubesException):
self.call_mgmt_func(b'admin.vm.Clone',
b'test-vm1', b'', b'name=test-vm2 label=red')
self.assertNotIn('test-vm2', self.app.domains)
self.assertEqual(mock_clone.mock_calls, [])
self.assertFalse(os.path.exists(os.path.join(
self.test_base_dir, 'appvms', 'test-vm2')))
self.assertFalse(self.app.save.called)
@unittest.mock.patch('qubes.storage.Storage.clone')
@unittest.mock.patch('qubes.storage.Storage.verify')
def test_352_vm_clone_duplicate_name(self, mock_verify, mock_clone):
mock_clone.side_effect = self.dummy_coro
mock_verify.side_effect = self.dummy_coro
with self.assertRaises(qubes.exc.QubesException):
self.call_mgmt_func(b'admin.vm.Clone',
b'test-vm1', b'', b'name=test-vm1')
self.assertFalse(self.app.save.called)
@unittest.mock.patch('qubes.storage.Storage.clone')
@unittest.mock.patch('qubes.storage.Storage.verify')
def test_353_vm_clone_invalid_name(self, mock_verify, mock_clone):
mock_clone.side_effect = self.dummy_coro
mock_verify.side_effect = self.dummy_coro
with self.assertRaises(qubes.exc.QubesException):
self.call_mgmt_func(b'admin.vm.Clone',
b'test-vm1', b'', b'name=test-vm2/..')
self.assertNotIn('test-vm2/..', self.app.domains)
self.assertEqual(mock_clone.mock_calls, [])
self.assertFalse(os.path.exists(os.path.join(
self.test_base_dir, 'appvms', 'test-vm2/..')))
self.assertFalse(self.app.save.called)
def test_400_property_list(self): def test_400_property_list(self):
# actual function tested for admin.vm.property.* already # actual function tested for admin.vm.property.* already
@ -1598,93 +1534,100 @@ class TC_00_VMs(AdminAPITestCase):
self.call_mgmt_func(b'admin.vm.volume.Import', b'test-vm1', self.call_mgmt_func(b'admin.vm.volume.Import', b'test-vm1',
b'private') b'private')
def setup_for_clone(self):
self.pool = unittest.mock.MagicMock()
self.app.pools['test'] = self.pool
self.vm2 = self.app.add_new_vm('AppVM', label='red',
name='test-vm2',
template='test-template', kernel='')
self.pool.configure_mock(**{
'volumes': qubes.storage.VolumesCollection(self.pool),
'init_volume.return_value.pool': self.pool,
'__str__.return_value': 'test',
'get_volume.side_effect': (lambda vid:
self.vm.volumes['private']
if vid is self.vm.volumes['private'].vid
else self.vm2.volumes['private']
),
})
self.loop.run_until_complete(
self.vm.create_on_disk(pool='test'))
self.loop.run_until_complete(
self.vm2.create_on_disk(pool='test'))
# the call replaces self.vm.volumes[...] with result of import
# operation - make sure it stays as the same object
self.vm.volumes['private'].import_volume.return_value = \
self.vm.volumes['private']
self.vm2.volumes['private'].import_volume.return_value = \
self.vm2.volumes['private']
def test_520_vm_volume_clone(self): def test_520_vm_volume_clone(self):
self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2', self.setup_for_clone()
template='test-template') token = self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
self.vm.volumes = unittest.mock.MagicMock() b'test-vm1', b'private', b'')
self.vm2.volumes = unittest.mock.MagicMock() # token
volumes_conf = { self.assertEqual(len(token), 32)
'keys.return_value': ['root', 'private', 'volatile', 'kernel'], self.assertFalse(self.app.save.called)
} value = self.call_mgmt_func(b'admin.vm.volume.CloneTo',
self.vm.volumes.configure_mock(**volumes_conf) b'test-vm2', b'private', token.encode())
self.vm2.volumes.configure_mock(**volumes_conf) self.assertIsNone(value)
self.vm2.storage = unittest.mock.Mock() self.vm2.volumes['private'].import_volume.assert_called_once_with(
func_mock = unittest.mock.Mock() self.vm.volumes['private']
)
@asyncio.coroutine self.vm2.volumes['private'].import_volume.assert_called_once_with(
def coroutine_mock(*args, **kwargs): self.vm2.volumes['private']
return func_mock(*args, **kwargs) )
self.vm2.storage.clone_volume = coroutine_mock
self.call_mgmt_func(b'admin.vm.volume.Clone',
b'test-vm1', b'private', b'test-vm2')
self.assertEqual(self.vm.volumes.mock_calls,
[('keys', (), {}),
('__getitem__', ('private', ), {}),
('__getitem__().__hash__', (), {}),
])
self.assertEqual(self.vm2.volumes.mock_calls,
[unittest.mock.call.keys()])
self.assertEqual(self.vm2.storage.mock_calls, [])
self.assertEqual(func_mock.mock_calls, [
unittest.mock.call(self.vm, 'private')
])
self.app.save.assert_called_once_with() self.app.save.assert_called_once_with()
def test_521_vm_volume_clone_invalid_volume(self): def test_521_vm_volume_clone_invalid_volume(self):
self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2', self.setup_for_clone()
template='test-template')
self.vm.volumes = unittest.mock.MagicMock()
self.vm2.volumes = unittest.mock.MagicMock()
volumes_conf = {
'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm2.volumes.configure_mock(**volumes_conf)
self.vm2.storage = unittest.mock.Mock()
func_mock = unittest.mock.Mock()
@asyncio.coroutine
def coroutine_mock(*args, **kwargs):
return func_mock(*args, **kwargs)
self.vm2.storage.clone_volume = coroutine_mock
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
self.call_mgmt_func(b'admin.vm.volume.Clone', self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
b'test-vm1', b'private123', b'test-vm2') b'test-vm1', b'private123', None)
self.assertEqual(self.vm.volumes.mock_calls, self.assertNotIn('init_volume().import_volume',
[('keys', (), {})]) map(operator.itemgetter(0), self.pool.mock_calls))
self.assertEqual(self.vm2.volumes.mock_calls, [])
self.assertEqual(self.vm2.storage.mock_calls, [])
self.assertEqual(func_mock.mock_calls, [])
self.assertFalse(self.app.save.called) self.assertFalse(self.app.save.called)
def test_522_vm_volume_clone_invalid_vm(self): def test_522_vm_volume_clone_invalid_volume2(self):
self.vm2 = self.app.add_new_vm('AppVM', label='red', name='test-vm2', self.setup_for_clone()
template='test-template')
self.vm.volumes = unittest.mock.MagicMock()
self.vm2.volumes = unittest.mock.MagicMock()
volumes_conf = {
'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
}
self.vm.volumes.configure_mock(**volumes_conf)
self.vm2.volumes.configure_mock(**volumes_conf)
self.vm2.storage = unittest.mock.Mock()
func_mock = unittest.mock.Mock()
@asyncio.coroutine token = self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
def coroutine_mock(*args, **kwargs): b'test-vm1', b'private', b'')
return func_mock(*args, **kwargs) with self.assertRaises(AssertionError):
self.vm2.storage.clone_volume = coroutine_mock self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private123', token.encode())
self.assertNotIn('init_volume().import_volume',
map(operator.itemgetter(0), self.pool.mock_calls))
self.assertFalse(self.app.save.called)
def test_523_vm_volume_clone_removed_volume(self):
self.setup_for_clone()
token = self.call_mgmt_func(b'admin.vm.volume.CloneFrom',
b'test-vm1', b'private', b'')
def get_volume(vid):
if vid == self.vm.volumes['private']:
raise KeyError(vid)
else:
return unittest.mock.DEFAULT
self.pool.get_volume.side_effect = get_volume
with self.assertRaises(AssertionError):
self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private', token.encode())
self.assertNotIn('init_volume().import_volume',
map(operator.itemgetter(0), self.pool.mock_calls))
self.assertFalse(self.app.save.called)
def test_524_vm_volume_clone_invlid_token(self):
self.setup_for_clone()
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
self.call_mgmt_func(b'admin.vm.volume.Clone', self.call_mgmt_func(b'admin.vm.volume.CloneTo',
b'test-vm1', b'private123', b'no-such-vm') b'test-vm1', b'private', b'no-such-token')
self.assertEqual(self.vm.volumes.mock_calls, self.assertNotIn('init_volume().import_volume',
[('keys', (), {})]) map(operator.itemgetter(0), self.pool.mock_calls))
self.assertEqual(self.vm2.volumes.mock_calls, [])
self.assertEqual(self.vm2.storage.mock_calls, [])
self.assertEqual(func_mock.mock_calls, [])
self.assertFalse(self.app.save.called) self.assertFalse(self.app.save.called)
def test_530_tag_list(self): def test_530_tag_list(self):

View File

@ -137,7 +137,7 @@ class TC_00_ThinPool(ThinPoolBase):
volume.create() volume.create()
path = "/dev/%s" % volume.vid path = "/dev/%s" % volume.vid
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
self.pool.remove(volume) volume.remove()
def test_003_read_write_volume(self): def test_003_read_write_volume(self):
''' Test read-write volume creation ''' ''' Test read-write volume creation '''
@ -157,7 +157,7 @@ class TC_00_ThinPool(ThinPoolBase):
volume.create() volume.create()
path = "/dev/%s" % volume.vid path = "/dev/%s" % volume.vid
self.assertTrue(os.path.exists(path)) self.assertTrue(os.path.exists(path))
self.pool.remove(volume) volume.remove()
@skipUnlessLvmPoolExists @skipUnlessLvmPoolExists
class TC_01_ThinPool(qubes.tests.SystemTestsMixin, ThinPoolBase): class TC_01_ThinPool(qubes.tests.SystemTestsMixin, ThinPoolBase):