parent
94937d1085
commit
cd489f46e1
177
qubes/api/__init__.py
Normal file
177
qubes/api/__init__.py
Normal 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))
|
@ -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
|
@ -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
|
@ -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',
|
||||
):
|
||||
|
@ -23,13 +23,13 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import libvirt
|
||||
import unittest.mock
|
||||
|
||||
import libvirt
|
||||
|
||||
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
|
@ -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(
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user