Rename MgmtAPI to AdminAPI - part 1: classes

QubesOS/qubes-issues#853
This commit is contained in:
Marek Marczykowski-Górecki 2017-05-12 19:07:41 +02:00
parent 94937d1085
commit cd489f46e1
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
8 changed files with 259 additions and 229 deletions

177
qubes/api/__init__.py Normal file
View File

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

View File

@ -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.api
import qubes.vm.qubesvm
import qubes.storage import qubes.storage
import qubes.utils import qubes.utils
import qubes.vm
class ProtocolError(AssertionError): import qubes.vm.qubesvm
'''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))
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

View File

@ -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 QubesInternalAPI(qubes.api.AbstractQubesAPI):
class QubesInternalMgmt(qubes.mgmt.AbstractQubesMgmt):
''' 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

View File

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

View File

@ -23,13 +23,13 @@
import asyncio import asyncio
import os import os
import shutil import shutil
import libvirt
import unittest.mock import unittest.mock
import libvirt
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

View File

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

View File

@ -12,8 +12,9 @@ import traceback
import libvirtaio import libvirtaio
import qubes import qubes
import qubes.mgmt import qubes.api
import qubes.mgmtinternal 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')

View File

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