Browse Source

Rename MgmtAPI to AdminAPI - part 1: classes

QubesOS/qubes-issues#853
Marek Marczykowski-Górecki 7 years ago
parent
commit
cd489f46e1

+ 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 asyncio
-import functools
 import string
 import string
 
 
 import pkg_resources
 import pkg_resources
 
 
-import qubes.vm
-import qubes.vm.qubesvm
+import qubes.api
 import qubes.storage
 import qubes.storage
 import qubes.utils
 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):
 class QubesMgmtEventsDispatcher(object):
@@ -195,13 +42,13 @@ class QubesMgmtEventsDispatcher(object):
     def vm_handler(self, subject, event, **kwargs):
     def vm_handler(self, subject, event, **kwargs):
         if event.startswith('mgmt-permission:'):
         if event.startswith('mgmt-permission:'):
             return
             return
-        if not list(apply_filters([(subject, event, kwargs)],
+        if not list(qubes.api.apply_filters([(subject, event, kwargs)],
                 self.filters)):
                 self.filters)):
             return
             return
         self.send_event(subject, event, **kwargs)
         self.send_event(subject, event, **kwargs)
 
 
     def app_handler(self, 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)):
                 self.filters)):
             return
             return
         self.send_event(subject, event, **kwargs)
         self.send_event(subject, event, **kwargs)
@@ -215,7 +62,7 @@ class QubesMgmtEventsDispatcher(object):
         vm.remove_handler('*', self.vm_handler)
         vm.remove_handler('*', self.vm_handler)
 
 
 
 
-class QubesMgmt(AbstractQubesMgmt):
+class QubesAdminAPI(qubes.api.AbstractQubesAPI):
     '''Implementation of Qubes Management API calls
     '''Implementation of Qubes Management API calls
 
 
     This class contains all the methods available in the main API.
     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/
         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
     @asyncio.coroutine
     def vmclass_list(self):
     def vmclass_list(self):
         '''List all VM classes'''
         '''List all VM classes'''
@@ -237,7 +84,7 @@ class QubesMgmt(AbstractQubesMgmt):
         return ''.join('{}\n'.format(ep.name)
         return ''.join('{}\n'.format(ep.name)
             for ep in entrypoints)
             for ep in entrypoints)
 
 
-    @api('mgmt.vm.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.List', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_list(self):
     def vm_list(self):
         '''List all the domains'''
         '''List all the domains'''
@@ -254,7 +101,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 vm.get_power_state())
                 vm.get_power_state())
             for vm in sorted(domains))
             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
     @asyncio.coroutine
     def vm_property_list(self):
     def vm_property_list(self):
         '''List all properties on a qube'''
         '''List all properties on a qube'''
@@ -264,7 +111,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return ''.join('{}\n'.format(prop.__name__) for prop in properties)
         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
     @asyncio.coroutine
     def vm_property_get(self):
     def vm_property_get(self):
         '''Get a value of one property'''
         '''Get a value of one property'''
@@ -295,7 +142,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 property_type,
                 property_type,
                 str(value) if value is not None else '')
                 str(value) if value is not None else '')
 
 
-    @api('mgmt.vm.property.Set')
+    @qubes.api.method('mgmt.vm.property.Set')
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_set(self, untrusted_payload):
     def vm_property_set(self, untrusted_payload):
         assert self.arg in self.dest.property_list()
         assert self.arg in self.dest.property_list()
@@ -308,7 +155,7 @@ class QubesMgmt(AbstractQubesMgmt):
         setattr(self.dest, self.arg, newvalue)
         setattr(self.dest, self.arg, newvalue)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.property.Help', no_payload=True)
+    @qubes.api.method('mgmt.vm.property.Help', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_help(self):
     def vm_property_help(self):
         '''Get help for one property'''
         '''Get help for one property'''
@@ -323,7 +170,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return qubes.utils.format_doc(doc)
         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
     @asyncio.coroutine
     def vm_property_reset(self):
     def vm_property_reset(self):
         '''Reset a property to a default value'''
         '''Reset a property to a default value'''
@@ -334,7 +181,7 @@ class QubesMgmt(AbstractQubesMgmt):
         delattr(self.dest, self.arg)
         delattr(self.dest, self.arg)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.volume.List', no_payload=True)
+    @qubes.api.method('mgmt.vm.volume.List', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_list(self):
     def vm_volume_list(self):
         assert not self.arg
         assert not self.arg
@@ -342,7 +189,7 @@ class QubesMgmt(AbstractQubesMgmt):
         volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
         volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
         return ''.join('{}\n'.format(name) for name in volume_names)
         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
     @asyncio.coroutine
     def vm_volume_info(self):
     def vm_volume_info(self):
         assert self.arg in self.dest.volumes.keys()
         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
         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
             volume_properties)
             volume_properties)
 
 
-    @api('mgmt.vm.volume.ListSnapshots', no_payload=True)
+    @qubes.api.method('mgmt.vm.volume.ListSnapshots', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_listsnapshots(self):
     def vm_volume_listsnapshots(self):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -368,7 +215,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return ''.join('{}\n'.format(revision) for revision in revisions)
         return ''.join('{}\n'.format(revision) for revision in revisions)
 
 
-    @api('mgmt.vm.volume.Revert')
+    @qubes.api.method('mgmt.vm.volume.Revert')
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_revert(self, untrusted_payload):
     def vm_volume_revert(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -385,7 +232,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.dest.storage.get_pool(volume).revert(revision)
         self.dest.storage.get_pool(volume).revert(revision)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.volume.Resize')
+    @qubes.api.method('mgmt.vm.volume.Resize')
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_resize(self, untrusted_payload):
     def vm_volume_resize(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -401,7 +248,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.dest.storage.resize(self.arg, size)
         self.dest.storage.resize(self.arg, size)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.pool.List', no_payload=True)
+    @qubes.api.method('mgmt.pool.List', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_list(self):
     def pool_list(self):
         assert not self.arg
         assert not self.arg
@@ -411,7 +258,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return ''.join('{}\n'.format(pool) for pool in pools)
         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
     @asyncio.coroutine
     def pool_listdrivers(self):
     def pool_listdrivers(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -424,7 +271,7 @@ class QubesMgmt(AbstractQubesMgmt):
             ' '.join(qubes.storage.driver_parameters(driver)))
             ' '.join(qubes.storage.driver_parameters(driver)))
             for driver in drivers)
             for driver in drivers)
 
 
-    @api('mgmt.pool.Info', no_payload=True)
+    @qubes.api.method('mgmt.pool.Info', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_info(self):
     def pool_info(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -437,7 +284,7 @@ class QubesMgmt(AbstractQubesMgmt):
         return ''.join('{}={}\n'.format(prop, val)
         return ''.join('{}={}\n'.format(prop, val)
             for prop, val in sorted(pool.config.items()))
             for prop, val in sorted(pool.config.items()))
 
 
-    @api('mgmt.pool.Add')
+    @qubes.api.method('mgmt.pool.Add')
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_add(self, untrusted_payload):
     def pool_add(self, untrusted_payload):
         assert self.dest.name == 'dom0'
         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.add_pool(name=pool_name, driver=self.arg, **pool_config)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.pool.Remove', no_payload=True)
+    @qubes.api.method('mgmt.pool.Remove', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_remove(self):
     def pool_remove(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -483,7 +330,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.app.remove_pool(self.arg)
         self.app.remove_pool(self.arg)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.label.List', no_payload=True)
+    @qubes.api.method('mgmt.label.List', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_list(self):
     def label_list(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -493,7 +340,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return ''.join('{}\n'.format(label.name) for label in labels)
         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
     @asyncio.coroutine
     def label_get(self):
     def label_get(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -507,7 +354,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return label.color
         return label.color
 
 
-    @api('mgmt.label.Index', no_payload=True)
+    @qubes.api.method('mgmt.label.Index', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_index(self):
     def label_index(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -521,7 +368,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
         return str(label.index)
         return str(label.index)
 
 
-    @api('mgmt.label.Create')
+    @qubes.api.method('mgmt.label.Create')
     @asyncio.coroutine
     @asyncio.coroutine
     def label_create(self, untrusted_payload):
     def label_create(self, untrusted_payload):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -557,7 +404,7 @@ class QubesMgmt(AbstractQubesMgmt):
         self.app.labels[new_index] = label
         self.app.labels[new_index] = label
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.label.Remove', no_payload=True)
+    @qubes.api.method('mgmt.label.Remove', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_remove(self):
     def label_remove(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -579,42 +426,42 @@ class QubesMgmt(AbstractQubesMgmt):
         del self.app.labels[label.index]
         del self.app.labels[label.index]
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.Start', no_payload=True)
+    @qubes.api.method('mgmt.vm.Start', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_start(self):
     def vm_start(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.start()
         yield from self.dest.start()
 
 
-    @api('mgmt.vm.Shutdown', no_payload=True)
+    @qubes.api.method('mgmt.vm.Shutdown', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_shutdown(self):
     def vm_shutdown(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.shutdown()
         yield from self.dest.shutdown()
 
 
-    @api('mgmt.vm.Pause', no_payload=True)
+    @qubes.api.method('mgmt.vm.Pause', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_pause(self):
     def vm_pause(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.pause()
         yield from self.dest.pause()
 
 
-    @api('mgmt.vm.Unpause', no_payload=True)
+    @qubes.api.method('mgmt.vm.Unpause', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_unpause(self):
     def vm_unpause(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.unpause()
         yield from self.dest.unpause()
 
 
-    @api('mgmt.vm.Kill', no_payload=True)
+    @qubes.api.method('mgmt.vm.Kill', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_kill(self):
     def vm_kill(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.kill()
         yield from self.dest.kill()
 
 
-    @api('mgmt.Events', no_payload=True)
+    @qubes.api.method('mgmt.Events', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def events(self):
     def events(self):
         assert not self.arg
         assert not self.arg
@@ -655,14 +502,14 @@ class QubesMgmt(AbstractQubesMgmt):
         else:
         else:
             self.dest.remove_handler('*', dispatcher.vm_handler)
             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
     @asyncio.coroutine
     def vm_feature_list(self):
     def vm_feature_list(self):
         assert not self.arg
         assert not self.arg
         features = self.fire_event_for_filter(self.dest.features.keys())
         features = self.fire_event_for_filter(self.dest.features.keys())
         return ''.join('{}\n'.format(feature) for feature in features)
         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
     @asyncio.coroutine
     def vm_feature_get(self):
     def vm_feature_get(self):
         # validation of self.arg done by qrexec-policy is enough
         # 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)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
         return value
 
 
-    @api('mgmt.vm.feature.CheckWithTemplate', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.CheckWithTemplate', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_checkwithtemplate(self):
     def vm_feature_checkwithtemplate(self):
         # validation of self.arg done by qrexec-policy is enough
         # 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)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
         return value
 
 
-    @api('mgmt.vm.feature.Remove', no_payload=True)
+    @qubes.api.method('mgmt.vm.feature.Remove', no_payload=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_remove(self):
     def vm_feature_remove(self):
         # validation of self.arg done by qrexec-policy is enough
         # 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)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.feature.Set')
+    @qubes.api.method('mgmt.vm.feature.Set')
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_set(self, untrusted_payload):
     def vm_feature_set(self, untrusted_payload):
         # validation of self.arg done by qrexec-policy is enough
         # validation of self.arg done by qrexec-policy is enough
@@ -709,14 +556,14 @@ class QubesMgmt(AbstractQubesMgmt):
         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
+    @qubes.api.method('mgmt.vm.Create.{endpoint}', endpoints=(ep.name
             for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
             for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_create(self, endpoint, untrusted_payload=None):
     def vm_create(self, endpoint, untrusted_payload=None):
         return self._vm_create(endpoint, allow_pool=False,
         return self._vm_create(endpoint, allow_pool=False,
             untrusted_payload=untrusted_payload)
             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)))
             for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_create_in_pool(self, endpoint, untrusted_payload=None):
     def vm_create_in_pool(self, endpoint, untrusted_payload=None):
@@ -746,7 +593,7 @@ class QubesMgmt(AbstractQubesMgmt):
                 errors='strict').split(' '):
                 errors='strict').split(' '):
             untrusted_key, untrusted_value = untrusted_param.split('=', 1)
             untrusted_key, untrusted_value = untrusted_param.split('=', 1)
             if untrusted_key in kwargs:
             if untrusted_key in kwargs:
-                raise ProtocolError('duplicated parameters')
+                raise qubes.api.ProtocolError('duplicated parameters')
 
 
             if untrusted_key == 'name':
             if untrusted_key == 'name':
                 qubes.vm.validate_name(None, None, untrusted_value)
                 qubes.vm.validate_name(None, None, untrusted_value)
@@ -764,7 +611,7 @@ class QubesMgmt(AbstractQubesMgmt):
 
 
             elif untrusted_key == 'pool' and allow_pool:
             elif untrusted_key == 'pool' and allow_pool:
                 if pool is not None:
                 if pool is not None:
-                    raise ProtocolError('duplicated pool parameter')
+                    raise qubes.api.ProtocolError('duplicated pool parameter')
                 pool = self.app.get_pool(untrusted_value)
                 pool = self.app.get_pool(untrusted_value)
             elif untrusted_key.startswith('pool:') and allow_pool:
             elif untrusted_key.startswith('pool:') and allow_pool:
                 untrusted_volume = untrusted_key.split(':', 1)[1]
                 untrusted_volume = untrusted_key.split(':', 1)[1]
@@ -774,19 +621,19 @@ class QubesMgmt(AbstractQubesMgmt):
                     'kernel']
                     'kernel']
                 volume = untrusted_volume
                 volume = untrusted_volume
                 if volume in pools:
                 if volume in pools:
-                    raise ProtocolError(
+                    raise qubes.api.ProtocolError(
                         'duplicated pool:{} parameter'.format(volume))
                         'duplicated pool:{} parameter'.format(volume))
                 pools[volume] = self.app.get_pool(untrusted_value)
                 pools[volume] = self.app.get_pool(untrusted_value)
 
 
             else:
             else:
-                raise ProtocolError('Invalid param name')
+                raise qubes.api.ProtocolError('Invalid param name')
         del untrusted_payload
         del untrusted_payload
 
 
         if 'name' not in kwargs or 'label' not in kwargs:
         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:
         if pool and pools:
-            raise ProtocolError(
+            raise qubes.api.ProtocolError(
                 'Only one of \'pool=\' and \'pool:volume=\' can be used')
                 'Only one of \'pool=\' and \'pool:volume=\' can be used')
 
 
         if kwargs['name'] in self.app.domains:
         if kwargs['name'] in self.app.domains:
@@ -804,7 +651,7 @@ class QubesMgmt(AbstractQubesMgmt):
             raise
             raise
         self.app.save()
         self.app.save()
 
 
-    @api('mgmt.vm.Clone')
+    @qubes.api.method('mgmt.vm.Clone')
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_clone(self, untrusted_payload):
     def vm_clone(self, untrusted_payload):
         assert not self.arg
         assert not self.arg

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

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

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

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

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

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

+ 7 - 6
qubes/tools/qubesd.py

@@ -12,8 +12,9 @@ import traceback
 import libvirtaio
 import libvirtaio
 
 
 import qubes
 import qubes
-import qubes.mgmt
-import qubes.mgmtinternal
+import qubes.api
+import qubes.api.admin
+import qubes.api.internal
 import qubes.utils
 import qubes.utils
 import qubes.vm.qubesvm
 import qubes.vm.qubesvm
 
 
@@ -88,13 +89,13 @@ class QubesDaemonProtocol(asyncio.Protocol):
 
 
         # except clauses will fall through to transport.abort() below
         # except clauses will fall through to transport.abort() below
 
 
-        except qubes.mgmt.PermissionDenied:
+        except qubes.api.PermissionDenied:
             self.app.log.warning(
             self.app.log.warning(
                 'permission denied for call %s+%s (%s → %s) '
                 'permission denied for call %s+%s (%s → %s) '
                 'with payload of %d bytes',
                 'with payload of %d bytes',
                     method, arg, src, dest, len(untrusted_payload))
                     method, arg, src, dest, len(untrusted_payload))
 
 
-        except qubes.mgmt.ProtocolError:
+        except qubes.api.ProtocolError:
             self.app.log.warning(
             self.app.log.warning(
                 'protocol error for call %s+%s (%s → %s) '
                 'protocol error for call %s+%s (%s → %s) '
                 'with payload of %d bytes',
                 'with payload of %d bytes',
@@ -194,7 +195,7 @@ def main(args=None):
         pass
         pass
     old_umask = os.umask(0o007)
     old_umask = os.umask(0o007)
     server = loop.run_until_complete(loop.create_unix_server(
     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))
             app=args.app), QUBESD_SOCK))
     shutil.chown(QUBESD_SOCK, group='qubes')
     shutil.chown(QUBESD_SOCK, group='qubes')
 
 
@@ -204,7 +205,7 @@ def main(args=None):
         pass
         pass
     server_internal = loop.run_until_complete(loop.create_unix_server(
     server_internal = loop.run_until_complete(loop.create_unix_server(
         functools.partial(QubesDaemonProtocol,
         functools.partial(QubesDaemonProtocol,
-            qubes.mgmtinternal.QubesInternalMgmt,
+            qubes.api.internal.QubesInternalAPI,
             app=args.app), QUBESD_INTERNAL_SOCK))
             app=args.app), QUBESD_INTERNAL_SOCK))
     shutil.chown(QUBESD_INTERNAL_SOCK, group='qubes')
     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/exc.py
 %{python3_sitelib}/qubes/firewall.py
 %{python3_sitelib}/qubes/firewall.py
 %{python3_sitelib}/qubes/log.py
 %{python3_sitelib}/qubes/log.py
-%{python3_sitelib}/qubes/mgmt.py
-%{python3_sitelib}/qubes/mgmtinternal.py
 %{python3_sitelib}/qubes/rngdoc.py
 %{python3_sitelib}/qubes/rngdoc.py
 %{python3_sitelib}/qubes/tarwriter.py
 %{python3_sitelib}/qubes/tarwriter.py
 %{python3_sitelib}/qubes/utils.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
 %dir %{python3_sitelib}/qubes/vm/__pycache__
 %dir %{python3_sitelib}/qubes/vm/__pycache__
 %{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/run.py
 %{python3_sitelib}/qubes/tests/extra.py
 %{python3_sitelib}/qubes/tests/extra.py
 
 
+%{python3_sitelib}/qubes/tests/api_admin.py
 %{python3_sitelib}/qubes/tests/app.py
 %{python3_sitelib}/qubes/tests/app.py
 %{python3_sitelib}/qubes/tests/devices.py
 %{python3_sitelib}/qubes/tests/devices.py
 %{python3_sitelib}/qubes/tests/events.py
 %{python3_sitelib}/qubes/tests/events.py
 %{python3_sitelib}/qubes/tests/firewall.py
 %{python3_sitelib}/qubes/tests/firewall.py
 %{python3_sitelib}/qubes/tests/init.py
 %{python3_sitelib}/qubes/tests/init.py
-%{python3_sitelib}/qubes/tests/mgmt.py
 %{python3_sitelib}/qubes/tests/storage.py
 %{python3_sitelib}/qubes/tests/storage.py
 %{python3_sitelib}/qubes/tests/storage_file.py
 %{python3_sitelib}/qubes/tests/storage_file.py
 %{python3_sitelib}/qubes/tests/storage_lvm.py
 %{python3_sitelib}/qubes/tests/storage_lvm.py