api/admin: add admin.vm.CreateDisposable in place of internal.vm.Create.DispVM
Add public Admin API call to create Disposable VM that would be automatically destroyed after shutdown. Do not keep this functionality for qrexec-policy tool only. Also, use admin.vm.Start there, instead of internal.vm.Start and admin.vm.Kill instead of internal.vm.CleanupDispVM (this is enough, because DispVM now have auto_cleanup property). QubesOS/qubes-issues#2974
This commit is contained in:
parent
691a6f4d8c
commit
971c7d4ac9
1
Makefile
1
Makefile
@ -43,6 +43,7 @@ ADMIN_API_METHODS_SIMPLE = \
|
|||||||
admin.vm.CreateInPool.DispVM \
|
admin.vm.CreateInPool.DispVM \
|
||||||
admin.vm.CreateInPool.StandaloneVM \
|
admin.vm.CreateInPool.StandaloneVM \
|
||||||
admin.vm.CreateInPool.TemplateVM \
|
admin.vm.CreateInPool.TemplateVM \
|
||||||
|
admin.vm.CreateDisposable \
|
||||||
admin.vm.Kill \
|
admin.vm.Kill \
|
||||||
admin.vm.List \
|
admin.vm.List \
|
||||||
admin.vm.Pause \
|
admin.vm.Pause \
|
||||||
|
@ -915,6 +915,25 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
|
|||||||
raise
|
raise
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
|
@qubes.api.method('admin.vm.CreateDisposable', no_payload=True,
|
||||||
|
scope='global', write=True)
|
||||||
|
@asyncio.coroutine
|
||||||
|
def create_disposable(self):
|
||||||
|
assert not self.arg
|
||||||
|
|
||||||
|
if self.dest.name == 'dom0':
|
||||||
|
dispvm_template = self.src.default_dispvm
|
||||||
|
else:
|
||||||
|
dispvm_template = self.dest
|
||||||
|
|
||||||
|
self.fire_event_for_permission(dispvm_template=dispvm_template)
|
||||||
|
|
||||||
|
dispvm = yield from qubes.vm.dispvm.DispVM.from_appvm(dispvm_template)
|
||||||
|
# TODO: move this to extension (in race-free fashion, better than here)
|
||||||
|
dispvm.tags.add('disp-created-by-' + str(self.src))
|
||||||
|
|
||||||
|
return dispvm.name
|
||||||
|
|
||||||
@qubes.api.method('admin.vm.Remove', no_payload=True,
|
@qubes.api.method('admin.vm.Remove', no_payload=True,
|
||||||
scope='global', write=True)
|
scope='global', write=True)
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -55,30 +55,6 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
|
|||||||
|
|
||||||
return json.dumps(system_info)
|
return json.dumps(system_info)
|
||||||
|
|
||||||
@qubes.api.method('internal.vm.Start', no_payload=True)
|
|
||||||
@asyncio.coroutine
|
|
||||||
def start(self):
|
|
||||||
assert not self.arg
|
|
||||||
if self.dest.name == 'dom0':
|
|
||||||
return
|
|
||||||
|
|
||||||
yield from self.dest.start()
|
|
||||||
|
|
||||||
@qubes.api.method('internal.vm.Create.DispVM', no_payload=True)
|
|
||||||
@asyncio.coroutine
|
|
||||||
def create_dispvm(self):
|
|
||||||
assert not self.arg
|
|
||||||
|
|
||||||
dispvm = yield from qubes.vm.dispvm.DispVM.from_appvm(self.dest)
|
|
||||||
return dispvm.name
|
|
||||||
|
|
||||||
@qubes.api.method('internal.vm.CleanupDispVM', no_payload=True)
|
|
||||||
@asyncio.coroutine
|
|
||||||
def cleanup_dispvm(self):
|
|
||||||
assert not self.arg
|
|
||||||
|
|
||||||
yield from self.dest.cleanup()
|
|
||||||
|
|
||||||
@qubes.api.method('internal.vm.volume.ImportEnd')
|
@qubes.api.method('internal.vm.volume.ImportEnd')
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def vm_volume_import_end(self, untrusted_payload):
|
def vm_volume_import_end(self, untrusted_payload):
|
||||||
|
@ -2099,6 +2099,39 @@ class TC_00_VMs(AdminAPITestCase):
|
|||||||
memory_kb=stats2[2]['memory_kb']),
|
memory_kb=stats2[2]['memory_kb']),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_640_vm_create_disposable(self, mock_storage):
|
||||||
|
mock_storage.side_effect = self.dummy_coro
|
||||||
|
self.vm.dispvm_allowed = True
|
||||||
|
retval = self.call_mgmt_func(b'admin.vm.CreateDisposable',
|
||||||
|
b'test-vm1')
|
||||||
|
self.assertTrue(retval.startswith('disp'))
|
||||||
|
self.assertIn(retval, self.app.domains)
|
||||||
|
dispvm = self.app.domains[retval]
|
||||||
|
self.assertEqual(dispvm.template, self.vm)
|
||||||
|
mock_storage.assert_called_once_with()
|
||||||
|
self.assertTrue(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_641_vm_create_disposable_default(self, mock_storage):
|
||||||
|
mock_storage.side_effect = self.dummy_coro
|
||||||
|
self.vm.dispvm_allowed = True
|
||||||
|
self.app.default_dispvm = self.vm
|
||||||
|
retval = self.call_mgmt_func(b'admin.vm.CreateDisposable',
|
||||||
|
b'dom0')
|
||||||
|
self.assertTrue(retval.startswith('disp'))
|
||||||
|
mock_storage.assert_called_once_with()
|
||||||
|
self.assertTrue(self.app.save.called)
|
||||||
|
|
||||||
|
@unittest.mock.patch('qubes.storage.Storage.create')
|
||||||
|
def test_642_vm_create_disposable_not_allowed(self, storage_mock):
|
||||||
|
storage_mock.side_effect = self.dummy_coro
|
||||||
|
with self.assertRaises(qubes.exc.QubesException):
|
||||||
|
self.call_mgmt_func(b'admin.vm.CreateDisposable',
|
||||||
|
b'test-vm1')
|
||||||
|
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'admin.vm.List',
|
b'admin.vm.List',
|
||||||
|
@ -33,6 +33,7 @@ QREXEC_CLIENT = '/usr/lib/qubes/qrexec-client'
|
|||||||
QUBES_RPC_MULTIPLEXER_PATH = '/usr/lib/qubes/qubes-rpc-multiplexer'
|
QUBES_RPC_MULTIPLEXER_PATH = '/usr/lib/qubes/qubes-rpc-multiplexer'
|
||||||
POLICY_DIR = '/etc/qubes-rpc/policy'
|
POLICY_DIR = '/etc/qubes-rpc/policy'
|
||||||
QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
|
QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
|
||||||
|
QUBESD_SOCK = '/var/run/qubesd.sock'
|
||||||
|
|
||||||
|
|
||||||
class AccessDenied(Exception):
|
class AccessDenied(Exception):
|
||||||
@ -434,9 +435,9 @@ class PolicyAction(object):
|
|||||||
:return: name of new Disposable VM
|
:return: name of new Disposable VM
|
||||||
'''
|
'''
|
||||||
base_appvm = self.target.split(':', 1)[1]
|
base_appvm = self.target.split(':', 1)[1]
|
||||||
dispvm_name = qubesd_call(base_appvm, 'internal.vm.Create.DispVM')
|
dispvm_name = qubesd_call(base_appvm, 'admin.vm.CreateDisposable')
|
||||||
dispvm_name = dispvm_name.decode('ascii')
|
dispvm_name = dispvm_name.decode('ascii')
|
||||||
qubesd_call(dispvm_name, 'internal.vm.Start')
|
qubesd_call(dispvm_name, 'admin.vm.Start')
|
||||||
return dispvm_name
|
return dispvm_name
|
||||||
|
|
||||||
def ensure_target_running(self):
|
def ensure_target_running(self):
|
||||||
@ -445,8 +446,10 @@ class PolicyAction(object):
|
|||||||
|
|
||||||
:return: None
|
:return: None
|
||||||
'''
|
'''
|
||||||
|
if self.target == 'dom0':
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
qubesd_call(self.target, 'internal.vm.Start')
|
qubesd_call(self.target, 'admin.vm.Start')
|
||||||
except QubesMgmtException as e:
|
except QubesMgmtException as e:
|
||||||
if e.exc_type == 'QubesVMNotHaltedError':
|
if e.exc_type == 'QubesVMNotHaltedError':
|
||||||
pass
|
pass
|
||||||
@ -461,7 +464,7 @@ class PolicyAction(object):
|
|||||||
:param dispvm: name of Disposable VM
|
:param dispvm: name of Disposable VM
|
||||||
:return: None
|
:return: None
|
||||||
'''
|
'''
|
||||||
qubesd_call(dispvm, 'internal.vm.CleanupDispVM')
|
qubesd_call(dispvm, 'admin.vm.Kill')
|
||||||
|
|
||||||
|
|
||||||
class Policy(object):
|
class Policy(object):
|
||||||
@ -631,9 +634,13 @@ class QubesMgmtException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def qubesd_call(dest, method, arg=None, payload=None):
|
def qubesd_call(dest, method, arg=None, payload=None):
|
||||||
|
if method.startswith('internal.'):
|
||||||
|
socket_path = QUBESD_INTERNAL_SOCK
|
||||||
|
else:
|
||||||
|
socket_path = QUBESD_SOCK
|
||||||
try:
|
try:
|
||||||
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
client_socket.connect(QUBESD_INTERNAL_SOCK)
|
client_socket.connect(socket_path)
|
||||||
except IOError:
|
except IOError:
|
||||||
# TODO:
|
# TODO:
|
||||||
raise
|
raise
|
||||||
|
@ -461,7 +461,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
|
|||||||
'test-vm2', rule, 'test-vm2')
|
'test-vm2', rule, 'test-vm2')
|
||||||
action.execute('some-ident')
|
action.execute('some-ident')
|
||||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||||
[unittest.mock.call('test-vm2', 'internal.vm.Start')])
|
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||||
self.assertEqual(mock_subprocess.mock_calls,
|
self.assertEqual(mock_subprocess.mock_calls,
|
||||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
||||||
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
||||||
@ -473,8 +473,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
|
|||||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||||
'dom0', rule, 'dom0')
|
'dom0', rule, 'dom0')
|
||||||
action.execute('some-ident')
|
action.execute('some-ident')
|
||||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
self.assertEqual(mock_qubesd_call.mock_calls, [])
|
||||||
[unittest.mock.call('dom0', 'internal.vm.Start')])
|
|
||||||
self.assertEqual(mock_subprocess.mock_calls,
|
self.assertEqual(mock_subprocess.mock_calls,
|
||||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dom0',
|
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dom0',
|
||||||
'-c', 'some-ident',
|
'-c', 'some-ident',
|
||||||
@ -488,14 +487,13 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
|
|||||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||||
'$dispvm:default-dvm', rule, '$dispvm:default-dvm')
|
'$dispvm:default-dvm', rule, '$dispvm:default-dvm')
|
||||||
mock_qubesd_call.side_effect = (lambda target, call:
|
mock_qubesd_call.side_effect = (lambda target, call:
|
||||||
b'dispvm-name' if call == 'internal.vm.Create.DispVM' else
|
b'dispvm-name' if call == 'admin.vm.CreateDisposable' else
|
||||||
unittest.mock.DEFAULT)
|
unittest.mock.DEFAULT)
|
||||||
action.execute('some-ident')
|
action.execute('some-ident')
|
||||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||||
[unittest.mock.call('default-dvm', 'internal.vm.Create.DispVM'),
|
[unittest.mock.call('default-dvm', 'admin.vm.CreateDisposable'),
|
||||||
unittest.mock.call('dispvm-name', 'internal.vm.Start'),
|
unittest.mock.call('dispvm-name', 'admin.vm.Start'),
|
||||||
unittest.mock.call('dispvm-name',
|
unittest.mock.call('dispvm-name', 'admin.vm.Kill')])
|
||||||
'internal.vm.CleanupDispVM')])
|
|
||||||
self.assertEqual(mock_subprocess.mock_calls,
|
self.assertEqual(mock_subprocess.mock_calls,
|
||||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dispvm-name',
|
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dispvm-name',
|
||||||
'-c', 'some-ident', '-W',
|
'-c', 'some-ident', '-W',
|
||||||
@ -512,7 +510,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
|
|||||||
qubespolicy.QubesMgmtException('QubesVMNotHaltedError')
|
qubespolicy.QubesMgmtException('QubesVMNotHaltedError')
|
||||||
action.execute('some-ident')
|
action.execute('some-ident')
|
||||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||||
[unittest.mock.call('test-vm2', 'internal.vm.Start')])
|
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||||
self.assertEqual(mock_subprocess.mock_calls,
|
self.assertEqual(mock_subprocess.mock_calls,
|
||||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
||||||
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
||||||
@ -529,7 +527,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
|
|||||||
with self.assertRaises(qubespolicy.QubesMgmtException):
|
with self.assertRaises(qubespolicy.QubesMgmtException):
|
||||||
action.execute('some-ident')
|
action.execute('some-ident')
|
||||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||||
[unittest.mock.call('test-vm2', 'internal.vm.Start')])
|
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||||
self.assertEqual(mock_subprocess.mock_calls, [])
|
self.assertEqual(mock_subprocess.mock_calls, [])
|
||||||
|
|
||||||
class TC_20_Policy(qubes.tests.QubesTestCase):
|
class TC_20_Policy(qubes.tests.QubesTestCase):
|
||||||
@ -755,14 +753,14 @@ class TC_30_Misc(qubes.tests.QubesTestCase):
|
|||||||
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
||||||
}
|
}
|
||||||
mock_socket.configure_mock(**mock_config)
|
mock_socket.configure_mock(**mock_config)
|
||||||
result = qubespolicy.qubesd_call('test', 'method')
|
result = qubespolicy.qubesd_call('test', 'internal.method')
|
||||||
self.assertEqual(result, b'data')
|
self.assertEqual(result, b'data')
|
||||||
self.assertEqual(mock_socket.mock_calls, [
|
self.assertEqual(mock_socket.mock_calls, [
|
||||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||||
unittest.mock.call().sendall(b'dom0'),
|
unittest.mock.call().sendall(b'dom0'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'method'),
|
unittest.mock.call().sendall(b'internal.method'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'test'),
|
unittest.mock.call().sendall(b'test'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
@ -778,14 +776,15 @@ class TC_30_Misc(qubes.tests.QubesTestCase):
|
|||||||
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
||||||
}
|
}
|
||||||
mock_socket.configure_mock(**mock_config)
|
mock_socket.configure_mock(**mock_config)
|
||||||
result = qubespolicy.qubesd_call('test', 'method', 'arg', b'payload')
|
result = qubespolicy.qubesd_call('test', 'internal.method', 'arg',
|
||||||
|
b'payload')
|
||||||
self.assertEqual(result, b'data')
|
self.assertEqual(result, b'data')
|
||||||
self.assertEqual(mock_socket.mock_calls, [
|
self.assertEqual(mock_socket.mock_calls, [
|
||||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||||
unittest.mock.call().sendall(b'dom0'),
|
unittest.mock.call().sendall(b'dom0'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'method'),
|
unittest.mock.call().sendall(b'internal.method'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'test'),
|
unittest.mock.call().sendall(b'test'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
@ -805,14 +804,14 @@ class TC_30_Misc(qubes.tests.QubesTestCase):
|
|||||||
}
|
}
|
||||||
mock_socket.configure_mock(**mock_config)
|
mock_socket.configure_mock(**mock_config)
|
||||||
with self.assertRaises(qubespolicy.QubesMgmtException) as e:
|
with self.assertRaises(qubespolicy.QubesMgmtException) as e:
|
||||||
qubespolicy.qubesd_call('test', 'method')
|
qubespolicy.qubesd_call('test', 'internal.method')
|
||||||
self.assertEqual(e.exception.exc_type, 'SomeError')
|
self.assertEqual(e.exception.exc_type, 'SomeError')
|
||||||
self.assertEqual(mock_socket.mock_calls, [
|
self.assertEqual(mock_socket.mock_calls, [
|
||||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||||
unittest.mock.call().sendall(b'dom0'),
|
unittest.mock.call().sendall(b'dom0'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'method'),
|
unittest.mock.call().sendall(b'internal.method'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
unittest.mock.call().sendall(b'test'),
|
unittest.mock.call().sendall(b'test'),
|
||||||
unittest.mock.call().sendall(b'\x00'),
|
unittest.mock.call().sendall(b'\x00'),
|
||||||
|
Loading…
Reference in New Issue
Block a user