Преглед на файлове

Rename MgmtAPI to AdminAPI - part 1: classes

QubesOS/qubes-issues#853
Marek Marczykowski-Górecki преди 7 години
родител
ревизия
cd489f46e1
променени са 8 файла, в които са добавени 258 реда и са изтрити 228 реда
  1. 177 0
      qubes/api/__init__.py
  2. 48 201
      qubes/api/admin.py
  3. 7 8
      qubes/api/internal.py
  4. 1 1
      qubes/tests/__init__.py
  5. 7 7
      qubes/tests/api_admin.py
  6. 3 2
      qubes/tests/tools/qubesd.py
  7. 7 6
      qubes/tools/qubesd.py
  8. 8 3
      rpm_spec/core-dom0.spec

+ 177 - 0
qubes/api/__init__.py

@@ -0,0 +1,177 @@
+# -*- encoding: utf8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2017  Wojtek Porczyk <woju@invisiblethingslab.com>
+# Copyright (C) 2017  Marek Marczykowski-Górecki
+#                               <marmarek@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
+import asyncio
+import functools
+
+
+class ProtocolError(AssertionError):
+    '''Raised when something is wrong with data received'''
+    pass
+
+
+class PermissionDenied(Exception):
+    '''Raised deliberately by handlers when we decide not to cooperate'''
+    pass
+
+
+def method(name, *, no_payload=False, endpoints=None):
+    '''Decorator factory for methods intended to appear in API.
+
+    The decorated method can be called from public API using a child of
+    :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
+    called using remote management interface.
+
+    :param str name: qrexec rpc method name
+    :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
+        also will not pass payload at all to the method
+
+    The expected function method should have one argument (other than usual
+    *self*), ``untrusted_payload``, which will contain the payload.
+
+    .. warning::
+        This argument has to be named such, to remind the programmer that the
+        content of this variable is indeed untrusted.
+
+    If *no_payload* is true, then the method is called with no arguments.
+    '''
+
+    def decorator(func):
+        if no_payload:
+            # the following assignment is needed for how closures work in Python
+            _func = func
+            @functools.wraps(_func)
+            def wrapper(self, untrusted_payload, **kwargs):
+                if untrusted_payload != b'':
+                    raise ProtocolError('unexpected payload')
+                return _func(self, **kwargs)
+            func = wrapper
+
+        # 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 decorator
+
+
+def apply_filters(iterable, filters):
+    '''Apply filters returned by mgmt-permission:... event'''
+    for selector in filters:
+        iterable = filter(selector, iterable)
+    return iterable
+
+
+class AbstractQubesAPI(object):
+    '''Common code for Qubes Management Protocol handling
+
+    Different interfaces can expose different API call sets, however they share
+    common protocol and common implementation framework. This class is the
+    latter.
+
+    To implement a new interface, inherit from this class and write at least one
+    method and decorate it with :py:func:`api` decorator. It will have access to
+    pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
+    :py:attr:`arg` and :py:attr:`method`.
+
+    There are also two helper functions for firing events associated with API
+    calls.
+    '''
+    def __init__(self, app, src, method_name, dest, arg, send_event=None):
+        #: :py:class:`qubes.Qubes` object
+        self.app = app
+
+        #: source qube
+        self.src = self.app.domains[src.decode('ascii')]
+
+        #: destination qube
+        self.dest = self.app.domains[dest.decode('ascii')]
+
+        #: argument
+        self.arg = arg.decode('ascii')
+
+        #: name of the method
+        self.method = method_name.decode('ascii')
+
+        #: callback for sending events if applicable
+        self.send_event = send_event
+
+        #: is this operation cancellable?
+        self.cancellable = False
+
+        untrusted_candidates = []
+        for attr in dir(self):
+            func = getattr(self, attr)
+
+            if not callable(func):
+                continue
+
+            try:
+                # pylint: disable=protected-access
+                for mname, endpoint in func._rpcname:
+                    if mname != self.method:
+                        continue
+                    untrusted_candidates.append((func, endpoint))
+            except AttributeError:
+                continue
+
+        if not untrusted_candidates:
+            raise ProtocolError('no such method: {!r}'.format(self.method))
+
+        assert len(untrusted_candidates) == 1, \
+            'multiple candidates for method {!r}'.format(self.method)
+
+        #: the method to execute
+        self._handler = untrusted_candidates[0]
+        self._running_handler = None
+        del untrusted_candidates
+
+    def execute(self, *, untrusted_payload):
+        '''Execute management operation.
+
+        This method is a coroutine.
+        '''
+        handler, endpoint = self._handler
+        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
+
+    def cancel(self):
+        '''If operation is cancellable, interrupt it'''
+        if self.cancellable and self._running_handler is not None:
+            self._running_handler.cancel()
+
+
+    def fire_event_for_permission(self, **kwargs):
+        '''Fire an event on the source qube to check for permission'''
+        return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
+            dest=self.dest, arg=self.arg, **kwargs)
+
+    def fire_event_for_filter(self, iterable, **kwargs):
+        '''Fire an event on the source qube to filter for permission'''
+        return apply_filters(iterable,
+            self.fire_event_for_permission(**kwargs))

+ 48 - 201
qubes/mgmt.py → qubes/api/admin.py

@@ -23,168 +23,15 @@ Qubes OS Management API
 '''
 
 import asyncio
-import functools
 import string
 
 import pkg_resources
 
-import qubes.vm
-import qubes.vm.qubesvm
+import qubes.api
 import qubes.storage
 import qubes.utils
-
-class ProtocolError(AssertionError):
-    '''Raised when something is wrong with data received'''
-    pass
-
-class PermissionDenied(Exception):
-    '''Raised deliberately by handlers when we decide not to cooperate'''
-    pass
-
-
-def api(name, *, no_payload=False, endpoints=None):
-    '''Decorator factory for methods intended to appear in API.
-
-    The decorated method can be called from public API using a child of
-    :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
-    called using remote management interface.
-
-    :param str name: qrexec rpc method name
-    :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
-        also will not pass payload at all to the method
-
-    The expected function method should have one argument (other than usual
-    *self*), ``untrusted_payload``, which will contain the payload.
-
-    .. warning::
-        This argument has to be named such, to remind the programmer that the
-        content of this variable is indeed untrusted.
-
-    If *no_payload* is true, then the method is called with no arguments.
-    '''
-
-    def decorator(func):
-        if no_payload:
-            # the following assignment is needed for how closures work in Python
-            _func = func
-            @functools.wraps(_func)
-            def wrapper(self, untrusted_payload, **kwargs):
-                if untrusted_payload != b'':
-                    raise ProtocolError('unexpected payload')
-                return _func(self, **kwargs)
-            func = wrapper
-
-        # 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 decorator
-
-
-def apply_filters(iterable, filters):
-    '''Apply filters returned by mgmt-permission:... event'''
-    for selector in filters:
-        iterable = filter(selector, iterable)
-    return iterable
-
-
-class AbstractQubesMgmt(object):
-    '''Common code for Qubes Management Protocol handling
-
-    Different interfaces can expose different API call sets, however they share
-    common protocol and common implementation framework. This class is the
-    latter.
-
-    To implement a new interface, inherit from this class and write at least one
-    method and decorate it with :py:func:`api` decorator. It will have access to
-    pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
-    :py:attr:`arg` and :py:attr:`method`.
-
-    There are also two helper functions for firing events associated with API
-    calls.
-    '''
-    def __init__(self, app, src, method, dest, arg, send_event=None):
-        #: :py:class:`qubes.Qubes` object
-        self.app = app
-
-        #: source qube
-        self.src = self.app.domains[src.decode('ascii')]
-
-        #: destination qube
-        self.dest = self.app.domains[dest.decode('ascii')]
-
-        #: argument
-        self.arg = arg.decode('ascii')
-
-        #: name of the method
-        self.method = method.decode('ascii')
-
-        #: callback for sending events if applicable
-        self.send_event = send_event
-
-        #: is this operation cancellable?
-        self.cancellable = False
-
-        untrusted_candidates = []
-        for attr in dir(self):
-            func = getattr(self, attr)
-
-            if not callable(func):
-                continue
-
-            try:
-                # pylint: disable=protected-access
-                for method_name, endpoint in func._rpcname:
-                    if method_name != self.method:
-                        continue
-                    untrusted_candidates.append((func, endpoint))
-            except AttributeError:
-                continue
-
-        if not untrusted_candidates:
-            raise ProtocolError('no such method: {!r}'.format(self.method))
-
-        assert len(untrusted_candidates) == 1, \
-            'multiple candidates for method {!r}'.format(self.method)
-
-        #: the method to execute
-        self._handler = untrusted_candidates[0]
-        self._running_handler = None
-        del untrusted_candidates
-
-    def execute(self, *, untrusted_payload):
-        '''Execute management operation.
-
-        This method is a coroutine.
-        '''
-        handler, endpoint = self._handler
-        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
-
-    def cancel(self):
-        '''If operation is cancellable, interrupt it'''
-        if self.cancellable and self._running_handler is not None:
-            self._running_handler.cancel()
-
-
-    def fire_event_for_permission(self, **kwargs):
-        '''Fire an event on the source qube to check for permission'''
-        return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
-            dest=self.dest, arg=self.arg, **kwargs)
-
-    def fire_event_for_filter(self, iterable, **kwargs):
-        '''Fire an event on the source qube to filter for permission'''
-        return apply_filters(iterable,
-            self.fire_event_for_permission(**kwargs))
+import qubes.vm
+import qubes.vm.qubesvm
 
 
 class QubesMgmtEventsDispatcher(object):
@@ -195,13 +42,13 @@ class QubesMgmtEventsDispatcher(object):
     def vm_handler(self, subject, event, **kwargs):
         if event.startswith('mgmt-permission:'):
             return
-        if not list(apply_filters([(subject, event, kwargs)],
+        if not list(qubes.api.apply_filters([(subject, event, kwargs)],
                 self.filters)):
             return
         self.send_event(subject, event, **kwargs)
 
     def app_handler(self, subject, event, **kwargs):
-        if not list(apply_filters([(subject, event, kwargs)],
+        if not list(qubes.api.apply_filters([(subject, event, kwargs)],
                 self.filters)):
             return
         self.send_event(subject, event, **kwargs)
@@ -215,7 +62,7 @@ class QubesMgmtEventsDispatcher(object):
         vm.remove_handler('*', self.vm_handler)
 
 
-class QubesMgmt(AbstractQubesMgmt):
+class QubesAdminAPI(qubes.api.AbstractQubesAPI):
     '''Implementation of Qubes Management API calls
 
     This class contains all the methods available in the main API.
@@ -224,7 +71,7 @@ class QubesMgmt(AbstractQubesMgmt):
         https://www.qubes-os.org/doc/mgmt1/
     '''
 
-    @api('mgmt.vmclass.List', no_payload=True)
+    @qubes.api.method('mgmt.vmclass.List', no_payload=True)
     @asyncio.coroutine
     def vmclass_list(self):
         '''List all VM classes'''
@@ -237,7 +84,7 @@ class QubesMgmt(AbstractQubesMgmt):
         return ''.join('{}\n'.format(ep.name)
             for ep in entrypoints)
 
-    @api('mgmt.vm.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.List', no_payload=True)
     @asyncio.coroutine
     def vm_list(self):
         '''List all the domains'''
@@ -254,7 +101,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 vm.get_power_state())
             for vm in sorted(domains))
 
-    @api('mgmt.vm.property.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.property.List', no_payload=True)
     @asyncio.coroutine
     def vm_property_list(self):
         '''List all properties on a qube'''
@@ -264,7 +111,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return ''.join('{}\n'.format(prop.__name__) for prop in properties)
 
-    @api('mgmt.vm.property.Get', no_payload=True)
+    @qubes.api.method('mgmt.vm.property.Get', no_payload=True)
     @asyncio.coroutine
     def vm_property_get(self):
         '''Get a value of one property'''
@@ -295,7 +142,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 property_type,
                 str(value) if value is not None else '')
 
-    @api('mgmt.vm.property.Set')
+    @qubes.api.method('mgmt.vm.property.Set')
     @asyncio.coroutine
     def vm_property_set(self, untrusted_payload):
         assert self.arg in self.dest.property_list()
@@ -308,7 +155,7 @@ class QubesMgmt(AbstractQubesMgmt):
         setattr(self.dest, self.arg, newvalue)
         self.app.save()
 
-    @api('mgmt.vm.property.Help', no_payload=True)
+    @qubes.api.method('mgmt.vm.property.Help', no_payload=True)
     @asyncio.coroutine
     def vm_property_help(self):
         '''Get help for one property'''
@@ -323,7 +170,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return qubes.utils.format_doc(doc)
 
-    @api('mgmt.vm.property.Reset', no_payload=True)
+    @qubes.api.method('mgmt.vm.property.Reset', no_payload=True)
     @asyncio.coroutine
     def vm_property_reset(self):
         '''Reset a property to a default value'''
@@ -334,7 +181,7 @@ class QubesMgmt(AbstractQubesMgmt):
         delattr(self.dest, self.arg)
         self.app.save()
 
-    @api('mgmt.vm.volume.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.volume.List', no_payload=True)
     @asyncio.coroutine
     def vm_volume_list(self):
         assert not self.arg
@@ -342,7 +189,7 @@ class QubesMgmt(AbstractQubesMgmt):
         volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
         return ''.join('{}\n'.format(name) for name in volume_names)
 
-    @api('mgmt.vm.volume.Info', no_payload=True)
+    @qubes.api.method('mgmt.vm.volume.Info', no_payload=True)
     @asyncio.coroutine
     def vm_volume_info(self):
         assert self.arg in self.dest.volumes.keys()
@@ -357,7 +204,7 @@ class QubesMgmt(AbstractQubesMgmt):
         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
             volume_properties)
 
-    @api('mgmt.vm.volume.ListSnapshots', no_payload=True)
+    @qubes.api.method('mgmt.vm.volume.ListSnapshots', no_payload=True)
     @asyncio.coroutine
     def vm_volume_listsnapshots(self):
         assert self.arg in self.dest.volumes.keys()
@@ -368,7 +215,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return ''.join('{}\n'.format(revision) for revision in revisions)
 
-    @api('mgmt.vm.volume.Revert')
+    @qubes.api.method('mgmt.vm.volume.Revert')
     @asyncio.coroutine
     def vm_volume_revert(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
@@ -385,7 +232,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.dest.storage.get_pool(volume).revert(revision)
         self.app.save()
 
-    @api('mgmt.vm.volume.Resize')
+    @qubes.api.method('mgmt.vm.volume.Resize')
     @asyncio.coroutine
     def vm_volume_resize(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
@@ -401,7 +248,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.dest.storage.resize(self.arg, size)
         self.app.save()
 
-    @api('mgmt.pool.List', no_payload=True)
+    @qubes.api.method('mgmt.pool.List', no_payload=True)
     @asyncio.coroutine
     def pool_list(self):
         assert not self.arg
@@ -411,7 +258,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return ''.join('{}\n'.format(pool) for pool in pools)
 
-    @api('mgmt.pool.ListDrivers', no_payload=True)
+    @qubes.api.method('mgmt.pool.ListDrivers', no_payload=True)
     @asyncio.coroutine
     def pool_listdrivers(self):
         assert self.dest.name == 'dom0'
@@ -424,7 +271,7 @@ class QubesMgmt(AbstractQubesMgmt):
             ' '.join(qubes.storage.driver_parameters(driver)))
             for driver in drivers)
 
-    @api('mgmt.pool.Info', no_payload=True)
+    @qubes.api.method('mgmt.pool.Info', no_payload=True)
     @asyncio.coroutine
     def pool_info(self):
         assert self.dest.name == 'dom0'
@@ -437,7 +284,7 @@ class QubesMgmt(AbstractQubesMgmt):
         return ''.join('{}={}\n'.format(prop, val)
             for prop, val in sorted(pool.config.items()))
 
-    @api('mgmt.pool.Add')
+    @qubes.api.method('mgmt.pool.Add')
     @asyncio.coroutine
     def pool_add(self, untrusted_payload):
         assert self.dest.name == 'dom0'
@@ -472,7 +319,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
         self.app.save()
 
-    @api('mgmt.pool.Remove', no_payload=True)
+    @qubes.api.method('mgmt.pool.Remove', no_payload=True)
     @asyncio.coroutine
     def pool_remove(self):
         assert self.dest.name == 'dom0'
@@ -483,7 +330,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.app.remove_pool(self.arg)
         self.app.save()
 
-    @api('mgmt.label.List', no_payload=True)
+    @qubes.api.method('mgmt.label.List', no_payload=True)
     @asyncio.coroutine
     def label_list(self):
         assert self.dest.name == 'dom0'
@@ -493,7 +340,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return ''.join('{}\n'.format(label.name) for label in labels)
 
-    @api('mgmt.label.Get', no_payload=True)
+    @qubes.api.method('mgmt.label.Get', no_payload=True)
     @asyncio.coroutine
     def label_get(self):
         assert self.dest.name == 'dom0'
@@ -507,7 +354,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return label.color
 
-    @api('mgmt.label.Index', no_payload=True)
+    @qubes.api.method('mgmt.label.Index', no_payload=True)
     @asyncio.coroutine
     def label_index(self):
         assert self.dest.name == 'dom0'
@@ -521,7 +368,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
         return str(label.index)
 
-    @api('mgmt.label.Create')
+    @qubes.api.method('mgmt.label.Create')
     @asyncio.coroutine
     def label_create(self, untrusted_payload):
         assert self.dest.name == 'dom0'
@@ -557,7 +404,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.app.labels[new_index] = label
         self.app.save()
 
-    @api('mgmt.label.Remove', no_payload=True)
+    @qubes.api.method('mgmt.label.Remove', no_payload=True)
     @asyncio.coroutine
     def label_remove(self):
         assert self.dest.name == 'dom0'
@@ -579,42 +426,42 @@ class QubesMgmt(AbstractQubesMgmt):
         del self.app.labels[label.index]
         self.app.save()
 
-    @api('mgmt.vm.Start', no_payload=True)
+    @qubes.api.method('mgmt.vm.Start', no_payload=True)
     @asyncio.coroutine
     def vm_start(self):
         assert not self.arg
         self.fire_event_for_permission()
         yield from self.dest.start()
 
-    @api('mgmt.vm.Shutdown', no_payload=True)
+    @qubes.api.method('mgmt.vm.Shutdown', no_payload=True)
     @asyncio.coroutine
     def vm_shutdown(self):
         assert not self.arg
         self.fire_event_for_permission()
         yield from self.dest.shutdown()
 
-    @api('mgmt.vm.Pause', no_payload=True)
+    @qubes.api.method('mgmt.vm.Pause', no_payload=True)
     @asyncio.coroutine
     def vm_pause(self):
         assert not self.arg
         self.fire_event_for_permission()
         yield from self.dest.pause()
 
-    @api('mgmt.vm.Unpause', no_payload=True)
+    @qubes.api.method('mgmt.vm.Unpause', no_payload=True)
     @asyncio.coroutine
     def vm_unpause(self):
         assert not self.arg
         self.fire_event_for_permission()
         yield from self.dest.unpause()
 
-    @api('mgmt.vm.Kill', no_payload=True)
+    @qubes.api.method('mgmt.vm.Kill', no_payload=True)
     @asyncio.coroutine
     def vm_kill(self):
         assert not self.arg
         self.fire_event_for_permission()
         yield from self.dest.kill()
 
-    @api('mgmt.Events', no_payload=True)
+    @qubes.api.method('mgmt.Events', no_payload=True)
     @asyncio.coroutine
     def events(self):
         assert not self.arg
@@ -655,14 +502,14 @@ class QubesMgmt(AbstractQubesMgmt):
         else:
             self.dest.remove_handler('*', dispatcher.vm_handler)
 
-    @api('mgmt.vm.feature.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.List', no_payload=True)
     @asyncio.coroutine
     def vm_feature_list(self):
         assert not self.arg
         features = self.fire_event_for_filter(self.dest.features.keys())
         return ''.join('{}\n'.format(feature) for feature in features)
 
-    @api('mgmt.vm.feature.Get', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.Get', no_payload=True)
     @asyncio.coroutine
     def vm_feature_get(self):
         # validation of self.arg done by qrexec-policy is enough
@@ -674,7 +521,7 @@ class QubesMgmt(AbstractQubesMgmt):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
 
-    @api('mgmt.vm.feature.CheckWithTemplate', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.CheckWithTemplate', no_payload=True)
     @asyncio.coroutine
     def vm_feature_checkwithtemplate(self):
         # validation of self.arg done by qrexec-policy is enough
@@ -686,7 +533,7 @@ class QubesMgmt(AbstractQubesMgmt):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
 
-    @api('mgmt.vm.feature.Remove', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.Remove', no_payload=True)
     @asyncio.coroutine
     def vm_feature_remove(self):
         # validation of self.arg done by qrexec-policy is enough
@@ -698,7 +545,7 @@ class QubesMgmt(AbstractQubesMgmt):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         self.app.save()
 
-    @api('mgmt.vm.feature.Set')
+    @qubes.api.method('mgmt.vm.feature.Set')
     @asyncio.coroutine
     def vm_feature_set(self, untrusted_payload):
         # validation of self.arg done by qrexec-policy is enough
@@ -709,14 +556,14 @@ class QubesMgmt(AbstractQubesMgmt):
         self.dest.features[self.arg] = value
         self.app.save()
 
-    @api('mgmt.vm.Create.{endpoint}', endpoints=(ep.name
+    @qubes.api.method('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
+    @qubes.api.method('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):
@@ -746,7 +593,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 errors='strict').split(' '):
             untrusted_key, untrusted_value = untrusted_param.split('=', 1)
             if untrusted_key in kwargs:
-                raise ProtocolError('duplicated parameters')
+                raise qubes.api.ProtocolError('duplicated parameters')
 
             if untrusted_key == 'name':
                 qubes.vm.validate_name(None, None, untrusted_value)
@@ -764,7 +611,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
             elif untrusted_key == 'pool' and allow_pool:
                 if pool is not None:
-                    raise ProtocolError('duplicated pool parameter')
+                    raise qubes.api.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]
@@ -774,19 +621,19 @@ class QubesMgmt(AbstractQubesMgmt):
                     'kernel']
                 volume = untrusted_volume
                 if volume in pools:
-                    raise ProtocolError(
+                    raise qubes.api.ProtocolError(
                         'duplicated pool:{} parameter'.format(volume))
                 pools[volume] = self.app.get_pool(untrusted_value)
 
             else:
-                raise ProtocolError('Invalid param name')
+                raise qubes.api.ProtocolError('Invalid param name')
         del untrusted_payload
 
         if 'name' not in kwargs or 'label' not in kwargs:
-            raise ProtocolError('Missing name or label')
+            raise qubes.api.ProtocolError('Missing name or label')
 
         if pool and pools:
-            raise ProtocolError(
+            raise qubes.api.ProtocolError(
                 'Only one of \'pool=\' and \'pool:volume=\' can be used')
 
         if kwargs['name'] in self.app.domains:
@@ -804,7 +651,7 @@ class QubesMgmt(AbstractQubesMgmt):
             raise
         self.app.save()
 
-    @api('mgmt.vm.Clone')
+    @qubes.api.method('mgmt.vm.Clone')
     @asyncio.coroutine
     def vm_clone(self, untrusted_payload):
         assert not self.arg

+ 7 - 8
qubes/mgmtinternal.py → qubes/api/internal.py

@@ -23,13 +23,12 @@
 import asyncio
 import json
 
-import qubes.mgmt
+import qubes.api
+import qubes.api.admin
 import qubes.vm.dispvm
 
-api = qubes.mgmt.api
 
-
-class QubesInternalMgmt(qubes.mgmt.AbstractQubesMgmt):
+class QubesInternalAPI(qubes.api.AbstractQubesAPI):
     ''' Communication interface for dom0 components,
     by design the input here is trusted.'''
     #
@@ -40,7 +39,7 @@ class QubesInternalMgmt(qubes.mgmt.AbstractQubesMgmt):
     # ACTUAL RPC CALLS
     #
 
-    @api('mgmtinternal.GetSystemInfo', no_payload=True)
+    @qubes.api.method('mgmtinternal.GetSystemInfo', no_payload=True)
     @asyncio.coroutine
     def getsysteminfo(self):
         assert self.dest.name == 'dom0'
@@ -59,14 +58,14 @@ class QubesInternalMgmt(qubes.mgmt.AbstractQubesMgmt):
 
         return json.dumps(system_info)
 
-    @api('mgmtinternal.vm.Start', no_payload=True)
+    @qubes.api.method('mgmtinternal.vm.Start', no_payload=True)
     @asyncio.coroutine
     def start(self):
         assert not self.arg
 
         yield from self.dest.start()
 
-    @api('mgmtinternal.vm.Create.DispVM', no_payload=True)
+    @qubes.api.method('mgmtinternal.vm.Create.DispVM', no_payload=True)
     @asyncio.coroutine
     def create_dispvm(self):
         assert not self.arg
@@ -75,7 +74,7 @@ class QubesInternalMgmt(qubes.mgmt.AbstractQubesMgmt):
         dispvm = qubes.vm.dispvm.DispVM.from_appvm(self.dest)
         return dispvm.name
 
-    @api('mgmtinternal.vm.CleanupDispVM', no_payload=True)
+    @qubes.api.method('mgmtinternal.vm.CleanupDispVM', no_payload=True)
     @asyncio.coroutine
     def cleanup_dispvm(self):
         assert not self.arg

+ 1 - 1
qubes/tests/__init__.py

@@ -905,7 +905,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
             'qubes.tests.vm.adminvm',
             'qubes.tests.app',
             'qubes.tests.tarwriter',
-            'qubes.tests.mgmt',
+            'qubes.tests.api_admin',
             'qubespolicy.tests',
             'qubes.tests.tools.qubesd',
             ):

+ 7 - 7
qubes/tests/mgmt.py → qubes/tests/api_admin.py

@@ -23,13 +23,13 @@
 import asyncio
 import os
 import shutil
+import unittest.mock
 
 import libvirt
-import unittest.mock
 
 import qubes
+import qubes.api.admin
 import qubes.tests
-import qubes.mgmt
 
 # properties defined in API
 volume_properties = [
@@ -37,7 +37,7 @@ volume_properties = [
     'save_on_stop', 'snap_on_start']
 
 
-class MgmtTestCase(qubes.tests.QubesTestCase):
+class AdminAPITestCase(qubes.tests.QubesTestCase):
     def setUp(self):
         super().setUp()
         app = qubes.Qubes('/tmp/qubes-test.xml', load=False)
@@ -76,10 +76,10 @@ class MgmtTestCase(qubes.tests.QubesTestCase):
         self.base_dir_patch.stop()
         if os.path.exists(self.test_base_dir):
             shutil.rmtree(self.test_base_dir)
-        super(MgmtTestCase, self).tearDown()
+        super(AdminAPITestCase, self).tearDown()
 
     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.api.admin.QubesAdminAPI(self.app, b'dom0', method, dest, arg)
 
         loop = asyncio.get_event_loop()
         response = loop.run_until_complete(
@@ -89,7 +89,7 @@ class MgmtTestCase(qubes.tests.QubesTestCase):
         return response
 
 
-class TC_00_VMs(MgmtTestCase):
+class TC_00_VMs(AdminAPITestCase):
     def test_000_vm_list(self):
         value = self.call_mgmt_func(b'mgmt.vm.List', b'dom0')
         self.assertEqual(value,
@@ -865,7 +865,7 @@ class TC_00_VMs(MgmtTestCase):
 
     def test_270_events(self):
         send_event = unittest.mock.Mock(spec=[])
-        mgmt_obj = qubes.mgmt.QubesMgmt(self.app, b'dom0', b'mgmt.Events',
+        mgmt_obj = qubes.api.admin.QubesAdminAPI(self.app, b'dom0', b'mgmt.Events',
             b'dom0', b'', send_event=send_event)
 
         @asyncio.coroutine

+ 3 - 2
qubes/tests/tools/qubesd.py

@@ -22,7 +22,8 @@ import asyncio
 import socket
 import unittest.mock
 
-import qubes.mgmt
+import qubes.api
+import qubes.api.admin
 import qubes.tests
 import qubes.tools.qubesd
 
@@ -44,7 +45,7 @@ class TestMgmt(object):
                 'mgmt.event': self.event,
             }[self.method.decode()]
         except KeyError:
-            raise qubes.mgmt.ProtocolError('Invalid method')
+            raise qubes.api.ProtocolError('Invalid method')
 
     def execute(self, untrusted_payload):
         self.task = asyncio.Task(self.function(

+ 7 - 6
qubes/tools/qubesd.py

@@ -12,8 +12,9 @@ import traceback
 import libvirtaio
 
 import qubes
-import qubes.mgmt
-import qubes.mgmtinternal
+import qubes.api
+import qubes.api.admin
+import qubes.api.internal
 import qubes.utils
 import qubes.vm.qubesvm
 
@@ -88,13 +89,13 @@ class QubesDaemonProtocol(asyncio.Protocol):
 
         # except clauses will fall through to transport.abort() below
 
-        except qubes.mgmt.PermissionDenied:
+        except qubes.api.PermissionDenied:
             self.app.log.warning(
                 'permission denied for call %s+%s (%s → %s) '
                 'with payload of %d bytes',
                     method, arg, src, dest, len(untrusted_payload))
 
-        except qubes.mgmt.ProtocolError:
+        except qubes.api.ProtocolError:
             self.app.log.warning(
                 'protocol error for call %s+%s (%s → %s) '
                 'with payload of %d bytes',
@@ -194,7 +195,7 @@ def main(args=None):
         pass
     old_umask = os.umask(0o007)
     server = loop.run_until_complete(loop.create_unix_server(
-        functools.partial(QubesDaemonProtocol, qubes.mgmt.QubesMgmt,
+        functools.partial(QubesDaemonProtocol, qubes.api.admin.QubesAdminAPI,
             app=args.app), QUBESD_SOCK))
     shutil.chown(QUBESD_SOCK, group='qubes')
 
@@ -204,7 +205,7 @@ def main(args=None):
         pass
     server_internal = loop.run_until_complete(loop.create_unix_server(
         functools.partial(QubesDaemonProtocol,
-            qubes.mgmtinternal.QubesInternalMgmt,
+            qubes.api.internal.QubesInternalAPI,
             app=args.app), QUBESD_INTERNAL_SOCK))
     shutil.chown(QUBESD_INTERNAL_SOCK, group='qubes')
 

+ 8 - 3
rpm_spec/core-dom0.spec

@@ -247,12 +247,17 @@ fi
 %{python3_sitelib}/qubes/exc.py
 %{python3_sitelib}/qubes/firewall.py
 %{python3_sitelib}/qubes/log.py
-%{python3_sitelib}/qubes/mgmt.py
-%{python3_sitelib}/qubes/mgmtinternal.py
 %{python3_sitelib}/qubes/rngdoc.py
 %{python3_sitelib}/qubes/tarwriter.py
 %{python3_sitelib}/qubes/utils.py
 
+%dir %{python3_sitelib}/qubes/api
+%dir %{python3_sitelib}/qubes/api/__pycache__
+%{python3_sitelib}/qubes/api/__pycache__/*
+%{python3_sitelib}/qubes/api/__init__.py
+%{python3_sitelib}/qubes/api/internal.py
+%{python3_sitelib}/qubes/api/admin.py
+
 %dir %{python3_sitelib}/qubes/vm
 %dir %{python3_sitelib}/qubes/vm/__pycache__
 %{python3_sitelib}/qubes/vm/__pycache__/*
@@ -307,12 +312,12 @@ fi
 %{python3_sitelib}/qubes/tests/run.py
 %{python3_sitelib}/qubes/tests/extra.py
 
+%{python3_sitelib}/qubes/tests/api_admin.py
 %{python3_sitelib}/qubes/tests/app.py
 %{python3_sitelib}/qubes/tests/devices.py
 %{python3_sitelib}/qubes/tests/events.py
 %{python3_sitelib}/qubes/tests/firewall.py
 %{python3_sitelib}/qubes/tests/init.py
-%{python3_sitelib}/qubes/tests/mgmt.py
 %{python3_sitelib}/qubes/tests/storage.py
 %{python3_sitelib}/qubes/tests/storage_file.py
 %{python3_sitelib}/qubes/tests/storage_lvm.py