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.StandaloneVM \
 | 
			
		||||
	admin.vm.CreateInPool.TemplateVM \
 | 
			
		||||
	admin.vm.CreateDisposable \
 | 
			
		||||
	admin.vm.Kill \
 | 
			
		||||
	admin.vm.List \
 | 
			
		||||
	admin.vm.Pause \
 | 
			
		||||
 | 
			
		||||
@ -915,6 +915,25 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 | 
			
		||||
            raise
 | 
			
		||||
        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,
 | 
			
		||||
        scope='global', write=True)
 | 
			
		||||
    @asyncio.coroutine
 | 
			
		||||
 | 
			
		||||
@ -55,30 +55,6 @@ class QubesInternalAPI(qubes.api.AbstractQubesAPI):
 | 
			
		||||
 | 
			
		||||
        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')
 | 
			
		||||
    @asyncio.coroutine
 | 
			
		||||
    def vm_volume_import_end(self, untrusted_payload):
 | 
			
		||||
 | 
			
		||||
@ -2099,6 +2099,39 @@ class TC_00_VMs(AdminAPITestCase):
 | 
			
		||||
                    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):
 | 
			
		||||
        methods_with_no_payload = [
 | 
			
		||||
            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'
 | 
			
		||||
POLICY_DIR = '/etc/qubes-rpc/policy'
 | 
			
		||||
QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
 | 
			
		||||
QUBESD_SOCK = '/var/run/qubesd.sock'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccessDenied(Exception):
 | 
			
		||||
@ -434,9 +435,9 @@ class PolicyAction(object):
 | 
			
		||||
        :return: name of new Disposable VM
 | 
			
		||||
        '''
 | 
			
		||||
        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')
 | 
			
		||||
        qubesd_call(dispvm_name, 'internal.vm.Start')
 | 
			
		||||
        qubesd_call(dispvm_name, 'admin.vm.Start')
 | 
			
		||||
        return dispvm_name
 | 
			
		||||
 | 
			
		||||
    def ensure_target_running(self):
 | 
			
		||||
@ -445,8 +446,10 @@ class PolicyAction(object):
 | 
			
		||||
 | 
			
		||||
        :return: None
 | 
			
		||||
        '''
 | 
			
		||||
        if self.target == 'dom0':
 | 
			
		||||
            return
 | 
			
		||||
        try:
 | 
			
		||||
            qubesd_call(self.target, 'internal.vm.Start')
 | 
			
		||||
            qubesd_call(self.target, 'admin.vm.Start')
 | 
			
		||||
        except QubesMgmtException as e:
 | 
			
		||||
            if e.exc_type == 'QubesVMNotHaltedError':
 | 
			
		||||
                pass
 | 
			
		||||
@ -461,7 +464,7 @@ class PolicyAction(object):
 | 
			
		||||
        :param dispvm: name of Disposable VM
 | 
			
		||||
        :return: None
 | 
			
		||||
        '''
 | 
			
		||||
        qubesd_call(dispvm, 'internal.vm.CleanupDispVM')
 | 
			
		||||
        qubesd_call(dispvm, 'admin.vm.Kill')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Policy(object):
 | 
			
		||||
@ -631,9 +634,13 @@ class QubesMgmtException(Exception):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def qubesd_call(dest, method, arg=None, payload=None):
 | 
			
		||||
    if method.startswith('internal.'):
 | 
			
		||||
        socket_path = QUBESD_INTERNAL_SOCK
 | 
			
		||||
    else:
 | 
			
		||||
        socket_path = QUBESD_SOCK
 | 
			
		||||
    try:
 | 
			
		||||
        client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 | 
			
		||||
        client_socket.connect(QUBESD_INTERNAL_SOCK)
 | 
			
		||||
        client_socket.connect(socket_path)
 | 
			
		||||
    except IOError:
 | 
			
		||||
        # TODO:
 | 
			
		||||
        raise
 | 
			
		||||
 | 
			
		||||
@ -461,7 +461,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
 | 
			
		||||
            'test-vm2', rule, 'test-vm2')
 | 
			
		||||
        action.execute('some-ident')
 | 
			
		||||
        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,
 | 
			
		||||
            [unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
 | 
			
		||||
             '-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',
 | 
			
		||||
            'dom0', rule, 'dom0')
 | 
			
		||||
        action.execute('some-ident')
 | 
			
		||||
        self.assertEqual(mock_qubesd_call.mock_calls,
 | 
			
		||||
            [unittest.mock.call('dom0', 'internal.vm.Start')])
 | 
			
		||||
        self.assertEqual(mock_qubesd_call.mock_calls, [])
 | 
			
		||||
        self.assertEqual(mock_subprocess.mock_calls,
 | 
			
		||||
            [unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dom0',
 | 
			
		||||
             '-c', 'some-ident',
 | 
			
		||||
@ -488,14 +487,13 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
 | 
			
		||||
        action = qubespolicy.PolicyAction('test.service', 'test-vm1',
 | 
			
		||||
            '$dispvm:default-dvm', rule, '$dispvm:default-dvm')
 | 
			
		||||
        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)
 | 
			
		||||
        action.execute('some-ident')
 | 
			
		||||
        self.assertEqual(mock_qubesd_call.mock_calls,
 | 
			
		||||
            [unittest.mock.call('default-dvm', 'internal.vm.Create.DispVM'),
 | 
			
		||||
             unittest.mock.call('dispvm-name', 'internal.vm.Start'),
 | 
			
		||||
             unittest.mock.call('dispvm-name',
 | 
			
		||||
                'internal.vm.CleanupDispVM')])
 | 
			
		||||
            [unittest.mock.call('default-dvm', 'admin.vm.CreateDisposable'),
 | 
			
		||||
             unittest.mock.call('dispvm-name', 'admin.vm.Start'),
 | 
			
		||||
             unittest.mock.call('dispvm-name', 'admin.vm.Kill')])
 | 
			
		||||
        self.assertEqual(mock_subprocess.mock_calls,
 | 
			
		||||
            [unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dispvm-name',
 | 
			
		||||
             '-c', 'some-ident', '-W',
 | 
			
		||||
@ -512,7 +510,7 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
 | 
			
		||||
            qubespolicy.QubesMgmtException('QubesVMNotHaltedError')
 | 
			
		||||
        action.execute('some-ident')
 | 
			
		||||
        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,
 | 
			
		||||
            [unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
 | 
			
		||||
             '-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):
 | 
			
		||||
            action.execute('some-ident')
 | 
			
		||||
        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, [])
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
        }
 | 
			
		||||
        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(mock_socket.mock_calls, [
 | 
			
		||||
            unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
 | 
			
		||||
            unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
 | 
			
		||||
            unittest.mock.call().sendall(b'dom0'),
 | 
			
		||||
            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'test'),
 | 
			
		||||
            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'
 | 
			
		||||
        }
 | 
			
		||||
        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(mock_socket.mock_calls, [
 | 
			
		||||
            unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
 | 
			
		||||
            unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
 | 
			
		||||
            unittest.mock.call().sendall(b'dom0'),
 | 
			
		||||
            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'test'),
 | 
			
		||||
            unittest.mock.call().sendall(b'\x00'),
 | 
			
		||||
@ -805,14 +804,14 @@ class TC_30_Misc(qubes.tests.QubesTestCase):
 | 
			
		||||
        }
 | 
			
		||||
        mock_socket.configure_mock(**mock_config)
 | 
			
		||||
        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(mock_socket.mock_calls, [
 | 
			
		||||
            unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
 | 
			
		||||
            unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
 | 
			
		||||
            unittest.mock.call().sendall(b'dom0'),
 | 
			
		||||
            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'test'),
 | 
			
		||||
            unittest.mock.call().sendall(b'\x00'),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user