qubes/mgmt: explicit method decorator and misc improvements
- Get rid of @not_in_api, exchange for explicit @api() decorator. - Old @no_payload decorator becomes an argument (keyword-only). - Factor out AbstractQubesMgmt class to be a base class for other mgmt backends. - Use async def instead of @asyncio.coroutine. QubesOS/qubes-issues#2622
This commit is contained in:
parent
1b9479837a
commit
c4ef02c377
224
qubes/mgmt.py
224
qubes/mgmt.py
@ -22,10 +22,10 @@
|
|||||||
Qubes OS Management API
|
Qubes OS Management API
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import asyncio
|
import functools
|
||||||
import string
|
import string
|
||||||
|
|
||||||
import functools
|
import pkg_resources
|
||||||
|
|
||||||
import qubes.vm.qubesvm
|
import qubes.vm.qubesvm
|
||||||
import qubes.storage
|
import qubes.storage
|
||||||
@ -40,30 +40,59 @@ class PermissionDenied(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def not_in_api(func):
|
def api(name, *, no_payload=False):
|
||||||
'''Decorator for methods not intended to appear in API.
|
'''Decorator factory for methods intended to appear in API.
|
||||||
|
|
||||||
The decorated method cannot be called from public API using
|
The decorated method can be called from public API using a child of
|
||||||
:py:class:`QubesMgmt` class. The method becomes "private", and can be
|
:py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
|
||||||
called only as a helper for other methods.
|
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.
|
||||||
'''
|
'''
|
||||||
func.not_in_api = True
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
# TODO regexp for vm/dev classess; supply regexp groups as untrusted_ kwargs
|
||||||
|
|
||||||
def no_payload(func):
|
def decorator(func):
|
||||||
@functools.wraps(func)
|
if no_payload:
|
||||||
def wrapper(self, untrusted_payload):
|
# the following assignment is needed for how closures work in Python
|
||||||
if untrusted_payload != b'':
|
_func = func
|
||||||
raise ProtocolError('unexpected payload')
|
@functools.wraps(_func)
|
||||||
return func(self)
|
def wrapper(self, untrusted_payload):
|
||||||
return wrapper
|
if untrusted_payload != b'':
|
||||||
|
raise ProtocolError('unexpected payload')
|
||||||
|
return _func(self)
|
||||||
|
func = wrapper
|
||||||
|
|
||||||
|
func._rpcname = name # pylint: disable=protected-access
|
||||||
|
return func
|
||||||
|
|
||||||
class QubesMgmt(object):
|
return decorator
|
||||||
'''Implementation of Qubes Management API calls
|
|
||||||
|
|
||||||
This class contains all the methods available in the API.
|
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):
|
def __init__(self, app, src, method, dest, arg):
|
||||||
#: :py:class:`qubes.Qubes` object
|
#: :py:class:`qubes.Qubes` object
|
||||||
@ -81,62 +110,55 @@ class QubesMgmt(object):
|
|||||||
#: name of the method
|
#: name of the method
|
||||||
self.method = method.decode('ascii')
|
self.method = method.decode('ascii')
|
||||||
|
|
||||||
untrusted_func_name = self.method
|
untrusted_candidates = []
|
||||||
if untrusted_func_name.startswith('mgmt.'):
|
for attr in dir(self):
|
||||||
untrusted_func_name = untrusted_func_name[5:]
|
untrusted_func = getattr(self, attr)
|
||||||
untrusted_func_name = untrusted_func_name.lower().replace('.', '_')
|
|
||||||
|
|
||||||
if untrusted_func_name.startswith('_') \
|
if not callable(untrusted_func):
|
||||||
or not '_' in untrusted_func_name:
|
continue
|
||||||
raise ProtocolError(
|
|
||||||
'possibly malicious function name: {!r}'.format(
|
|
||||||
untrusted_func_name))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
untrusted_func = getattr(self, untrusted_func_name)
|
# pylint: disable=protected-access
|
||||||
except AttributeError:
|
if untrusted_func._rpcname != self.method:
|
||||||
raise ProtocolError(
|
continue
|
||||||
'no such attribute: {!r}'.format(
|
except AttributeError:
|
||||||
untrusted_func_name))
|
continue
|
||||||
|
|
||||||
if not asyncio.iscoroutinefunction(untrusted_func):
|
untrusted_candidates.append(untrusted_func)
|
||||||
raise ProtocolError(
|
|
||||||
'no such method: {!r}'.format(
|
|
||||||
untrusted_func_name))
|
|
||||||
|
|
||||||
if getattr(untrusted_func, 'not_in_api', False):
|
if not untrusted_candidates:
|
||||||
raise ProtocolError(
|
raise ProtocolError('no such method: {!r}'.format(self.method))
|
||||||
'attempt to call private method: {!r}'.format(
|
|
||||||
untrusted_func_name))
|
|
||||||
|
|
||||||
self.execute = untrusted_func
|
assert len(untrusted_candidates) == 1, \
|
||||||
del untrusted_func_name
|
'multiple candidates for method {!r}'.format(self.method)
|
||||||
del untrusted_func
|
|
||||||
|
|
||||||
#
|
#: the method to execute
|
||||||
# PRIVATE METHODS, not to be called via RPC
|
self.execute = untrusted_candidates[0]
|
||||||
#
|
del untrusted_candidates
|
||||||
|
|
||||||
@not_in_api
|
|
||||||
def fire_event_for_permission(self, **kwargs):
|
def fire_event_for_permission(self, **kwargs):
|
||||||
'''Fire an event on the source qube to check for permission'''
|
'''Fire an event on the source qube to check for permission'''
|
||||||
return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
|
return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
|
||||||
dest=self.dest, arg=self.arg, **kwargs)
|
dest=self.dest, arg=self.arg, **kwargs)
|
||||||
|
|
||||||
@not_in_api
|
|
||||||
def fire_event_for_filter(self, iterable, **kwargs):
|
def fire_event_for_filter(self, iterable, **kwargs):
|
||||||
'''Fire an event on the source qube to filter for permission'''
|
'''Fire an event on the source qube to filter for permission'''
|
||||||
for selector in self.fire_event_for_permission(**kwargs):
|
for selector in self.fire_event_for_permission(**kwargs):
|
||||||
iterable = filter(selector, iterable)
|
iterable = filter(selector, iterable)
|
||||||
return iterable
|
return iterable
|
||||||
|
|
||||||
#
|
|
||||||
# ACTUAL RPC CALLS
|
|
||||||
#
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
class QubesMgmt(AbstractQubesMgmt):
|
||||||
@no_payload
|
'''Implementation of Qubes Management API calls
|
||||||
def vm_list(self):
|
|
||||||
|
This class contains all the methods available in the main API.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
https://www.qubes-os.org/doc/mgmt1/
|
||||||
|
'''
|
||||||
|
|
||||||
|
@api('mgmt.vm.List', no_payload=True)
|
||||||
|
async def vm_list(self):
|
||||||
'''List all the domains'''
|
'''List all the domains'''
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
|
|
||||||
@ -151,9 +173,8 @@ class QubesMgmt(object):
|
|||||||
vm.get_power_state())
|
vm.get_power_state())
|
||||||
for vm in sorted(domains))
|
for vm in sorted(domains))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.property.List', no_payload=True)
|
||||||
@no_payload
|
async def vm_property_list(self):
|
||||||
def vm_property_list(self):
|
|
||||||
'''List all properties on a qube'''
|
'''List all properties on a qube'''
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
|
|
||||||
@ -161,9 +182,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return ''.join('{}\n'.format(prop.__name__) for prop in properties)
|
return ''.join('{}\n'.format(prop.__name__) for prop in properties)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.property.Get', no_payload=True)
|
||||||
@no_payload
|
async def vm_property_get(self):
|
||||||
def vm_property_get(self):
|
|
||||||
'''Get a value of one property'''
|
'''Get a value of one property'''
|
||||||
assert self.arg in self.dest.property_list()
|
assert self.arg in self.dest.property_list()
|
||||||
|
|
||||||
@ -192,8 +212,8 @@ class QubesMgmt(object):
|
|||||||
property_type,
|
property_type,
|
||||||
str(value) if value is not None else '')
|
str(value) if value is not None else '')
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.property.Set')
|
||||||
def vm_property_set(self, untrusted_payload):
|
async def vm_property_set(self, untrusted_payload):
|
||||||
assert self.arg in self.dest.property_list()
|
assert self.arg in self.dest.property_list()
|
||||||
|
|
||||||
property_def = self.dest.property_get_def(self.arg)
|
property_def = self.dest.property_get_def(self.arg)
|
||||||
@ -204,9 +224,8 @@ class QubesMgmt(object):
|
|||||||
setattr(self.dest, self.arg, newvalue)
|
setattr(self.dest, self.arg, newvalue)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.property.Help', no_payload=True)
|
||||||
@no_payload
|
async def vm_property_help(self):
|
||||||
def vm_property_help(self):
|
|
||||||
'''Get help for one property'''
|
'''Get help for one property'''
|
||||||
assert self.arg in self.dest.property_list()
|
assert self.arg in self.dest.property_list()
|
||||||
|
|
||||||
@ -219,9 +238,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return qubes.utils.format_doc(doc)
|
return qubes.utils.format_doc(doc)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.property.Reset', no_payload=True)
|
||||||
@no_payload
|
async def vm_property_reset(self):
|
||||||
def vm_property_reset(self):
|
|
||||||
'''Reset a property to a default value'''
|
'''Reset a property to a default value'''
|
||||||
assert self.arg in self.dest.property_list()
|
assert self.arg in self.dest.property_list()
|
||||||
|
|
||||||
@ -230,17 +248,15 @@ class QubesMgmt(object):
|
|||||||
delattr(self.dest, self.arg)
|
delattr(self.dest, self.arg)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.volume.List', no_payload=True)
|
||||||
@no_payload
|
async def vm_volume_list(self):
|
||||||
def vm_volume_list(self):
|
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.volume.Info', no_payload=True)
|
||||||
@no_payload
|
async 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()
|
||||||
|
|
||||||
self.fire_event_for_permission()
|
self.fire_event_for_permission()
|
||||||
@ -253,9 +269,8 @@ class QubesMgmt(object):
|
|||||||
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)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.volume.ListSnapshots', no_payload=True)
|
||||||
@no_payload
|
async 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()
|
||||||
|
|
||||||
volume = self.dest.volumes[self.arg]
|
volume = self.dest.volumes[self.arg]
|
||||||
@ -264,8 +279,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return ''.join('{}\n'.format(revision) for revision in revisions)
|
return ''.join('{}\n'.format(revision) for revision in revisions)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.volume.Revert')
|
||||||
def vm_volume_revert(self, untrusted_payload):
|
async def vm_volume_revert(self, untrusted_payload):
|
||||||
assert self.arg in self.dest.volumes.keys()
|
assert self.arg in self.dest.volumes.keys()
|
||||||
untrusted_revision = untrusted_payload.decode('ascii').strip()
|
untrusted_revision = untrusted_payload.decode('ascii').strip()
|
||||||
del untrusted_payload
|
del untrusted_payload
|
||||||
@ -280,8 +295,8 @@ class QubesMgmt(object):
|
|||||||
self.dest.storage.get_pool(volume).revert(revision)
|
self.dest.storage.get_pool(volume).revert(revision)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.vm.volume.Resize')
|
||||||
def vm_volume_resize(self, untrusted_payload):
|
async def vm_volume_resize(self, untrusted_payload):
|
||||||
assert self.arg in self.dest.volumes.keys()
|
assert self.arg in self.dest.volumes.keys()
|
||||||
untrusted_size = untrusted_payload.decode('ascii').strip()
|
untrusted_size = untrusted_payload.decode('ascii').strip()
|
||||||
del untrusted_payload
|
del untrusted_payload
|
||||||
@ -295,9 +310,8 @@ class QubesMgmt(object):
|
|||||||
self.dest.storage.resize(self.arg, size)
|
self.dest.storage.resize(self.arg, size)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.pool.List', no_payload=True)
|
||||||
@no_payload
|
async def pool_list(self):
|
||||||
def pool_list(self):
|
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
|
|
||||||
@ -305,9 +319,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return ''.join('{}\n'.format(pool) for pool in pools)
|
return ''.join('{}\n'.format(pool) for pool in pools)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.pool.ListDrivers', no_payload=True)
|
||||||
@no_payload
|
async def pool_listdrivers(self):
|
||||||
def pool_listdrivers(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
|
|
||||||
@ -318,9 +331,8 @@ class QubesMgmt(object):
|
|||||||
' '.join(qubes.storage.driver_parameters(driver)))
|
' '.join(qubes.storage.driver_parameters(driver)))
|
||||||
for driver in drivers)
|
for driver in drivers)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.pool.Info', no_payload=True)
|
||||||
@no_payload
|
async def pool_info(self):
|
||||||
def pool_info(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
assert self.arg in self.app.pools.keys()
|
assert self.arg in self.app.pools.keys()
|
||||||
|
|
||||||
@ -331,8 +343,8 @@ class QubesMgmt(object):
|
|||||||
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()))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.pool.Add')
|
||||||
def pool_add(self, untrusted_payload):
|
async def pool_add(self, untrusted_payload):
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
drivers = qubes.storage.pool_drivers()
|
drivers = qubes.storage.pool_drivers()
|
||||||
assert self.arg in drivers
|
assert self.arg in drivers
|
||||||
@ -365,9 +377,8 @@ class QubesMgmt(object):
|
|||||||
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()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.pool.Remove', no_payload=True)
|
||||||
@no_payload
|
async def pool_remove(self):
|
||||||
def pool_remove(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
assert self.arg in self.app.pools.keys()
|
assert self.arg in self.app.pools.keys()
|
||||||
|
|
||||||
@ -376,9 +387,8 @@ class QubesMgmt(object):
|
|||||||
self.app.remove_pool(self.arg)
|
self.app.remove_pool(self.arg)
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.label.List', no_payload=True)
|
||||||
@no_payload
|
async def label_list(self):
|
||||||
def label_list(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
assert not self.arg
|
assert not self.arg
|
||||||
|
|
||||||
@ -386,9 +396,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return ''.join('{}\n'.format(label.name) for label in labels)
|
return ''.join('{}\n'.format(label.name) for label in labels)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.label.Get', no_payload=True)
|
||||||
@no_payload
|
async def label_get(self):
|
||||||
def label_get(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -400,8 +409,8 @@ class QubesMgmt(object):
|
|||||||
|
|
||||||
return label.color
|
return label.color
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.label.Create')
|
||||||
def label_create(self, untrusted_payload):
|
async def label_create(self, untrusted_payload):
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
|
|
||||||
# don't confuse label name with label index
|
# don't confuse label name with label index
|
||||||
@ -435,9 +444,8 @@ class QubesMgmt(object):
|
|||||||
self.app.labels[new_index] = label
|
self.app.labels[new_index] = label
|
||||||
self.app.save()
|
self.app.save()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@api('mgmt.label.Remove', no_payload=True)
|
||||||
@no_payload
|
async def label_remove(self):
|
||||||
def label_remove(self):
|
|
||||||
assert self.dest.name == 'dom0'
|
assert self.dest.name == 'dom0'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
Loading…
Reference in New Issue
Block a user