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:
Marek Marczykowski-Górecki 2017-08-06 12:35:35 +02:00
parent 691a6f4d8c
commit 971c7d4ac9
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
6 changed files with 80 additions and 45 deletions

View File

@ -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 \

View File

@ -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

View File

@ -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):

View File

@ -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',

View File

@ -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

View File

@ -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'),