mgmt: vm.Create* and vm.Clone
Create methods are dynamically created based on available VM classes. QubesOS/qubes-issues#2622
This commit is contained in:
parent
52c3753d61
commit
53be79b3b7
165
qubes/mgmt.py
165
qubes/mgmt.py
@ -31,7 +31,7 @@ import pkg_resources
|
|||||||
import qubes.vm
|
import qubes.vm
|
||||||
import qubes.vm.qubesvm
|
import qubes.vm.qubesvm
|
||||||
import qubes.storage
|
import qubes.storage
|
||||||
|
import qubes.utils
|
||||||
|
|
||||||
class ProtocolError(AssertionError):
|
class ProtocolError(AssertionError):
|
||||||
'''Raised when something is wrong with data received'''
|
'''Raised when something is wrong with data received'''
|
||||||
@ -42,7 +42,7 @@ class PermissionDenied(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def api(name, *, no_payload=False):
|
def api(name, *, no_payload=False, endpoints=None):
|
||||||
'''Decorator factory for methods intended to appear in API.
|
'''Decorator factory for methods intended to appear in API.
|
||||||
|
|
||||||
The decorated method can be called from public API using a child of
|
The decorated method can be called from public API using a child of
|
||||||
@ -63,20 +63,24 @@ def api(name, *, no_payload=False):
|
|||||||
If *no_payload* is true, then the method is called with no arguments.
|
If *no_payload* is true, then the method is called with no arguments.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# TODO regexp for vm/dev classess; supply regexp groups as untrusted_ kwargs
|
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
if no_payload:
|
if no_payload:
|
||||||
# the following assignment is needed for how closures work in Python
|
# the following assignment is needed for how closures work in Python
|
||||||
_func = func
|
_func = func
|
||||||
@functools.wraps(_func)
|
@functools.wraps(_func)
|
||||||
def wrapper(self, untrusted_payload):
|
def wrapper(self, untrusted_payload, **kwargs):
|
||||||
if untrusted_payload != b'':
|
if untrusted_payload != b'':
|
||||||
raise ProtocolError('unexpected payload')
|
raise ProtocolError('unexpected payload')
|
||||||
return _func(self)
|
return _func(self, **kwargs)
|
||||||
func = wrapper
|
func = wrapper
|
||||||
|
|
||||||
func._rpcname = name # pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
if endpoints is None:
|
||||||
|
func._rpcname = ((name, None),)
|
||||||
|
else:
|
||||||
|
func._rpcname = tuple(
|
||||||
|
(name.format(endpoint=endpoint), endpoint)
|
||||||
|
for endpoint in endpoints)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
@ -128,20 +132,20 @@ class AbstractQubesMgmt(object):
|
|||||||
|
|
||||||
untrusted_candidates = []
|
untrusted_candidates = []
|
||||||
for attr in dir(self):
|
for attr in dir(self):
|
||||||
untrusted_func = getattr(self, attr)
|
func = getattr(self, attr)
|
||||||
|
|
||||||
if not callable(untrusted_func):
|
if not callable(func):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
if untrusted_func._rpcname != self.method:
|
for method, endpoint in func._rpcname:
|
||||||
continue
|
if method != self.method:
|
||||||
|
continue
|
||||||
|
untrusted_candidates.append((func, endpoint))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
untrusted_candidates.append(untrusted_func)
|
|
||||||
|
|
||||||
if not untrusted_candidates:
|
if not untrusted_candidates:
|
||||||
raise ProtocolError('no such method: {!r}'.format(self.method))
|
raise ProtocolError('no such method: {!r}'.format(self.method))
|
||||||
|
|
||||||
@ -158,8 +162,12 @@ class AbstractQubesMgmt(object):
|
|||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
'''
|
'''
|
||||||
self._running_handler = asyncio.ensure_future(self._handler(
|
handler, endpoint = self._handler
|
||||||
untrusted_payload=untrusted_payload))
|
kwargs = {}
|
||||||
|
if endpoint is not None:
|
||||||
|
kwargs['endpoint'] = endpoint
|
||||||
|
self._running_handler = asyncio.ensure_future(handler(
|
||||||
|
untrusted_payload=untrusted_payload, **kwargs))
|
||||||
return self._running_handler
|
return self._running_handler
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
@ -700,3 +708,130 @@ class QubesMgmt(AbstractQubesMgmt):
|
|||||||
self.fire_event_for_permission(value=value)
|
self.fire_event_for_permission(value=value)
|
||||||
self.dest.features[self.arg] = value
|
self.dest.features[self.arg] = value
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
|
@api('mgmt.vm.Create.{endpoint}', endpoints=(ep.name
|
||||||
|
for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
|
||||||
|
@asyncio.coroutine
|
||||||
|
def vm_create(self, endpoint, untrusted_payload=None):
|
||||||
|
return self._vm_create(endpoint, allow_pool=False,
|
||||||
|
untrusted_payload=untrusted_payload)
|
||||||
|
|
||||||
|
@api('mgmt.vm.CreateInPool.{endpoint}', endpoints=(ep.name
|
||||||
|
for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
|
||||||
|
@asyncio.coroutine
|
||||||
|
def vm_create_in_pool(self, endpoint, untrusted_payload=None):
|
||||||
|
return self._vm_create(endpoint, allow_pool=True,
|
||||||
|
untrusted_payload=untrusted_payload)
|
||||||
|
|
||||||
|
def _vm_create(self, vm_type, allow_pool=False, untrusted_payload=None):
|
||||||
|
assert self.dest.name == 'dom0'
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
pool = None
|
||||||
|
pools = {}
|
||||||
|
|
||||||
|
# this will raise exception if none is found
|
||||||
|
vm_class = qubes.utils.get_entry_point_one(qubes.vm.VM_ENTRY_POINT,
|
||||||
|
vm_type)
|
||||||
|
|
||||||
|
# if argument is given, it needs to be a valid template, and only
|
||||||
|
# when given VM class do need a template
|
||||||
|
if hasattr(vm_class, 'template'):
|
||||||
|
assert self.arg in self.app.domains
|
||||||
|
kwargs['template'] = self.app.domains[self.arg]
|
||||||
|
else:
|
||||||
|
assert not self.arg
|
||||||
|
|
||||||
|
for untrusted_param in untrusted_payload.decode('ascii',
|
||||||
|
errors='strict').split(' '):
|
||||||
|
untrusted_key, untrusted_value = untrusted_param.split('=', 1)
|
||||||
|
if untrusted_key in kwargs:
|
||||||
|
raise ProtocolError('duplicated parameters')
|
||||||
|
|
||||||
|
if untrusted_key == 'name':
|
||||||
|
qubes.vm.validate_name(None, None, untrusted_value)
|
||||||
|
kwargs['name'] = untrusted_value
|
||||||
|
|
||||||
|
elif untrusted_key == 'label':
|
||||||
|
# don't confuse label name with label index
|
||||||
|
assert not untrusted_value.isdigit()
|
||||||
|
allowed_chars = string.ascii_letters + string.digits + '-_.'
|
||||||
|
assert all(c in allowed_chars for c in untrusted_value)
|
||||||
|
try:
|
||||||
|
kwargs['label'] = self.app.get_label(untrusted_value)
|
||||||
|
except KeyError:
|
||||||
|
raise qubes.exc.QubesValueError
|
||||||
|
|
||||||
|
elif untrusted_key == 'pool' and allow_pool:
|
||||||
|
if pool is not None:
|
||||||
|
raise ProtocolError('duplicated pool parameter')
|
||||||
|
pool = self.app.get_pool(untrusted_value)
|
||||||
|
elif untrusted_key.startswith('pool:') and allow_pool:
|
||||||
|
untrusted_volume = untrusted_key.split(':', 1)[1]
|
||||||
|
# kind of ugly, but actual list of volumes is available only
|
||||||
|
# after creating a VM
|
||||||
|
assert untrusted_volume in ['root', 'private', 'volatile',
|
||||||
|
'kernel']
|
||||||
|
volume = untrusted_volume
|
||||||
|
if volume in pools:
|
||||||
|
raise ProtocolError(
|
||||||
|
'duplicated pool:{} parameter'.format(volume))
|
||||||
|
pools[volume] = self.app.get_pool(untrusted_value)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ProtocolError('Invalid param name')
|
||||||
|
del untrusted_payload
|
||||||
|
|
||||||
|
if 'name' not in kwargs or 'label' not in kwargs:
|
||||||
|
raise ProtocolError('Missing name or label')
|
||||||
|
|
||||||
|
if pool and pools:
|
||||||
|
raise ProtocolError(
|
||||||
|
'Only one of \'pool=\' and \'pool:volume=\' can be used')
|
||||||
|
|
||||||
|
if kwargs['name'] in self.app.domains:
|
||||||
|
raise qubes.exc.QubesValueError(
|
||||||
|
'VM {} already exists'.format(kwargs['name']))
|
||||||
|
|
||||||
|
self.fire_event_for_permission(pool=pool, pools=pools, **kwargs)
|
||||||
|
|
||||||
|
vm = self.app.add_new_vm(vm_class, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield from vm.create_on_disk(pool=pool, pools=pools)
|
||||||
|
except:
|
||||||
|
del self.app.domains[vm]
|
||||||
|
raise
|
||||||
|
self.app.save()
|
||||||
|
|
||||||
|
@api('mgmt.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)
|
||||||
|
# TODO: tags
|
||||||
|
# TODO: features
|
||||||
|
# TODO: firewall
|
||||||
|
# TODO: persistent devices
|
||||||
|
yield from dst_vm.clone_disk_files(src_vm)
|
||||||
|
except:
|
||||||
|
del self.app.domains[dst_vm]
|
||||||
|
raise
|
||||||
|
self.app.save()
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
''' Tests for management calls endpoints '''
|
''' Tests for management calls endpoints '''
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
import libvirt
|
import libvirt
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
@ -46,6 +48,9 @@ class MgmtTestCase(qubes.tests.QubesTestCase):
|
|||||||
self.template = app.add_new_vm('TemplateVM', label='black',
|
self.template = app.add_new_vm('TemplateVM', label='black',
|
||||||
name='test-template')
|
name='test-template')
|
||||||
app.default_template = 'test-template'
|
app.default_template = 'test-template'
|
||||||
|
with qubes.tests.substitute_entry_points('qubes.storage',
|
||||||
|
'qubes.tests.storage'):
|
||||||
|
app.add_pool('test', driver='test')
|
||||||
app.save = unittest.mock.Mock()
|
app.save = unittest.mock.Mock()
|
||||||
self.vm = app.add_new_vm('AppVM', label='red', name='test-vm1',
|
self.vm = app.add_new_vm('AppVM', label='red', name='test-vm1',
|
||||||
template='test-template')
|
template='test-template')
|
||||||
@ -62,6 +67,17 @@ class MgmtTestCase(qubes.tests.QubesTestCase):
|
|||||||
self.app.domains[0].fire_event = self.emitter.fire_event
|
self.app.domains[0].fire_event = self.emitter.fire_event
|
||||||
self.app.domains[0].fire_event_pre = self.emitter.fire_event_pre
|
self.app.domains[0].fire_event_pre = self.emitter.fire_event_pre
|
||||||
|
|
||||||
|
self.test_base_dir = '/tmp/qubes-test-dir'
|
||||||
|
self.base_dir_patch = unittest.mock.patch.dict(qubes.config.system_path,
|
||||||
|
{'qubes_base_dir': self.test_base_dir})
|
||||||
|
self.base_dir_patch.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.base_dir_patch.stop()
|
||||||
|
if os.path.exists(self.test_base_dir):
|
||||||
|
shutil.rmtree(self.test_base_dir)
|
||||||
|
super(MgmtTestCase, self).tearDown()
|
||||||
|
|
||||||
def call_mgmt_func(self, method, dest, arg=b'', payload=b''):
|
def call_mgmt_func(self, method, dest, arg=b'', payload=b''):
|
||||||
mgmt_obj = qubes.mgmt.QubesMgmt(self.app, b'dom0', method, dest, arg)
|
mgmt_obj = qubes.mgmt.QubesMgmt(self.app, b'dom0', method, dest, arg)
|
||||||
|
|
||||||
@ -848,7 +864,7 @@ class TC_00_VMs(MgmtTestCase):
|
|||||||
func_mock.assert_called_once_with()
|
func_mock.assert_called_once_with()
|
||||||
|
|
||||||
def test_270_events(self):
|
def test_270_events(self):
|
||||||
send_event = unittest.mock.Mock()
|
send_event = unittest.mock.Mock(spec=[])
|
||||||
mgmt_obj = qubes.mgmt.QubesMgmt(self.app, b'dom0', b'mgmt.Events',
|
mgmt_obj = qubes.mgmt.QubesMgmt(self.app, b'dom0', b'mgmt.Events',
|
||||||
b'dom0', b'', send_event=send_event)
|
b'dom0', b'', send_event=send_event)
|
||||||
|
|
||||||
@ -945,6 +961,293 @@ class TC_00_VMs(MgmtTestCase):
|
|||||||
self.assertNotIn('test-feature', self.vm.features)
|
self.assertNotIn('test-feature', self.vm.features)
|
||||||
self.assertFalse(self.app.save.called)
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def dummy_coro(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_330_vm_create_standalone(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.StandaloneVM',
|
||||||
|
b'dom0', b'', b'name=test-vm2 label=red')
|
||||||
|
|
||||||
|
self.assertIn('test-vm2', self.app.domains)
|
||||||
|
vm = self.app.domains['test-vm2']
|
||||||
|
self.assertIsInstance(vm, qubes.vm.standalonevm.StandaloneVM)
|
||||||
|
self.assertEqual(vm.label, self.app.get_label('red'))
|
||||||
|
self.assertEqual(storage_mock.mock_calls,
|
||||||
|
[unittest.mock.call(self.app.domains['test-vm2']).create()])
|
||||||
|
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.create')
|
||||||
|
def test_331_vm_create_standalone_spurious_template(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.StandaloneVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertEqual(storage_mock.mock_calls, [])
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(
|
||||||
|
self.test_base_dir, 'appvms', 'test-vm2')))
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_332_vm_create_app(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red')
|
||||||
|
|
||||||
|
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(storage_mock.mock_calls,
|
||||||
|
[unittest.mock.call(self.app.domains['test-vm2']).create()])
|
||||||
|
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.create')
|
||||||
|
def test_333_vm_create_app_missing_template(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'', b'name=test-vm2 label=red')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertEqual(storage_mock.mock_calls, [])
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(
|
||||||
|
self.test_base_dir, 'appvms', 'test-vm2')))
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_334_vm_create_invalid_name(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesValueError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-###')
|
||||||
|
|
||||||
|
self.assertNotIn('test-###', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_335_vm_create_missing_name(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template', b'label=red')
|
||||||
|
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_336_vm_create_spurious_pool(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template',
|
||||||
|
b'name=test-vm2 label=red pool=default')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_337_vm_create_duplicate_name(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesException):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template',
|
||||||
|
b'name=test-vm1 label=red')
|
||||||
|
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_338_vm_create_name_twice(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.Create.AppVM',
|
||||||
|
b'dom0', b'test-template',
|
||||||
|
b'name=test-vm2 name=test-vm3 label=red')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertNotIn('test-vm3', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_340_vm_create_in_pool_app(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool=test')
|
||||||
|
|
||||||
|
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'])
|
||||||
|
# setting pool= affect only volumes actually created for this VM,
|
||||||
|
# not used from a template or so
|
||||||
|
self.assertEqual(vm.volume_config['root']['pool'], 'default')
|
||||||
|
self.assertEqual(vm.volume_config['private']['pool'], 'test')
|
||||||
|
self.assertEqual(vm.volume_config['volatile']['pool'], 'test')
|
||||||
|
self.assertEqual(vm.volume_config['kernel']['pool'], 'linux-kernel')
|
||||||
|
self.assertEqual(storage_mock.mock_calls,
|
||||||
|
[unittest.mock.call(self.app.domains['test-vm2']).create()])
|
||||||
|
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.create')
|
||||||
|
def test_341_vm_create_in_pool_private(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool:private=test')
|
||||||
|
|
||||||
|
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.volume_config['root']['pool'], 'default')
|
||||||
|
self.assertEqual(vm.volume_config['private']['pool'], 'test')
|
||||||
|
self.assertEqual(vm.volume_config['volatile']['pool'], 'default')
|
||||||
|
self.assertEqual(vm.volume_config['kernel']['pool'], 'linux-kernel')
|
||||||
|
self.assertEqual(storage_mock.mock_calls,
|
||||||
|
[unittest.mock.call(self.app.domains['test-vm2']).create()])
|
||||||
|
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.create')
|
||||||
|
def test_342_vm_create_in_pool_invalid_pool(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesException):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool=no-such-pool')
|
||||||
|
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_343_vm_create_in_pool_invalid_pool2(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesException):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool:private=no-such-pool')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_344_vm_create_in_pool_invalid_volume(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool:invalid=test')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_345_vm_create_in_pool_app_root(self, storage_mock):
|
||||||
|
# setting custom pool for 'root' volume of AppVM should not be
|
||||||
|
# allowed - this volume belongs to the template
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesException):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool:root=test')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
self.assertFalse(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_346_vm_create_in_pool_duplicate_pool(self, storage_mock):
|
||||||
|
# setting custom pool for 'root' volume of AppVM should not be
|
||||||
|
# allowed - this volume belongs to the template
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self.call_mgmt_func(b'mgmt.vm.CreateInPool.AppVM',
|
||||||
|
b'dom0', b'test-template', b'name=test-vm2 label=red '
|
||||||
|
b'pool=test pool:root=test')
|
||||||
|
|
||||||
|
self.assertNotIn('test-vm2', self.app.domains)
|
||||||
|
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'mgmt.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(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'mgmt.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'mgmt.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'mgmt.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_990_vm_unexpected_payload(self):
|
def test_990_vm_unexpected_payload(self):
|
||||||
methods_with_no_payload = [
|
methods_with_no_payload = [
|
||||||
b'mgmt.vm.List',
|
b'mgmt.vm.List',
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
#
|
#
|
||||||
|
import unittest.mock
|
||||||
import qubes.log
|
import qubes.log
|
||||||
from qubes.exc import QubesException
|
from qubes.exc import QubesException
|
||||||
from qubes.storage import pool_drivers
|
from qubes.storage import pool_drivers
|
||||||
@ -27,6 +27,14 @@ from qubes.tests import QubesTestCase
|
|||||||
# :pylint: disable=invalid-name
|
# :pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
class TestPool(unittest.mock.Mock):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TestPool, self).__init__(*args, spec=qubes.storage.Pool, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'test'
|
||||||
|
|
||||||
|
|
||||||
class TestVM(object):
|
class TestVM(object):
|
||||||
def __init__(self, test, template=None):
|
def __init__(self, test, template=None):
|
||||||
self.app = test.app
|
self.app = test.app
|
||||||
|
8
setup.py
8
setup.py
@ -56,5 +56,11 @@ if __name__ == '__main__':
|
|||||||
'file = qubes.storage.file:FilePool',
|
'file = qubes.storage.file:FilePool',
|
||||||
'linux-kernel = qubes.storage.kernels:LinuxKernel',
|
'linux-kernel = qubes.storage.kernels:LinuxKernel',
|
||||||
'lvm_thin = qubes.storage.lvm:ThinPool',
|
'lvm_thin = qubes.storage.lvm:ThinPool',
|
||||||
]
|
],
|
||||||
|
'qubes.tests.storage': [
|
||||||
|
'test = qubes.tests.storage:TestPool',
|
||||||
|
'file = qubes.storage.file:FilePool',
|
||||||
|
'linux-kernel = qubes.storage.kernels:LinuxKernel',
|
||||||
|
'lvm_thin = qubes.storage.lvm:ThinPool',
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user