2017-02-24 01:21:02 +01:00
|
|
|
# -*- encoding: utf8 -*-
|
|
|
|
#
|
|
|
|
# The Qubes OS Project, http://www.qubes-os.org
|
|
|
|
#
|
|
|
|
# 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 Lesser 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 Lesser General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Lesser General Public License along
|
|
|
|
# with this program; if not, see <http://www.gnu.org/licenses/>.
|
2017-02-25 20:56:31 +01:00
|
|
|
|
|
|
|
'''Base classes for managed objects'''
|
|
|
|
|
2017-05-11 23:21:04 +02:00
|
|
|
import qubesadmin.exc
|
2017-02-24 01:21:02 +01:00
|
|
|
|
|
|
|
DEFAULT = object()
|
|
|
|
|
2017-02-25 20:56:31 +01:00
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
class PropertyHolder(object):
|
|
|
|
'''A base class for object having properties retrievable using mgmt API.
|
|
|
|
|
|
|
|
Warning: each (non-private) local attribute needs to be defined at class
|
|
|
|
level, even if initialized in __init__; otherwise will be treated as
|
|
|
|
property retrievable using mgmt call.
|
|
|
|
'''
|
|
|
|
#: a place for appropriate Qubes() object (QubesLocal or QubesRemote),
|
|
|
|
# use None for self
|
|
|
|
app = None
|
|
|
|
|
|
|
|
def __init__(self, app, method_prefix, method_dest):
|
|
|
|
#: appropriate Qubes() object (QubesLocal or QubesRemote), use None
|
|
|
|
# for self
|
|
|
|
self.app = app
|
|
|
|
self._method_prefix = method_prefix
|
|
|
|
self._method_dest = method_dest
|
|
|
|
self._properties = None
|
|
|
|
self._properties_help = None
|
|
|
|
|
2017-05-24 04:24:22 +02:00
|
|
|
def qubesd_call(self, dest, method, arg=None, payload=None,
|
|
|
|
payload_stream=None):
|
2017-02-24 01:21:02 +01:00
|
|
|
'''
|
|
|
|
Call into qubesd using appropriate mechanism. This method should be
|
|
|
|
defined by a subclass.
|
|
|
|
|
2017-05-24 04:24:22 +02:00
|
|
|
Only one of `payload` and `payload_stream` can be specified.
|
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
:param dest: Destination VM name
|
2017-05-12 19:36:03 +02:00
|
|
|
:param method: Full API method name ('admin...')
|
2017-02-24 01:21:02 +01:00
|
|
|
:param arg: Method argument (if any)
|
|
|
|
:param payload: Payload send to the method
|
2017-05-24 04:24:22 +02:00
|
|
|
:param payload_stream: file-like object to read payload from
|
2017-02-24 01:21:02 +01:00
|
|
|
:return: Data returned by qubesd (string)
|
|
|
|
'''
|
2017-04-29 02:27:40 +02:00
|
|
|
if dest is None:
|
|
|
|
dest = self._method_dest
|
2017-02-24 01:21:02 +01:00
|
|
|
# have the actual implementation at Qubes() instance
|
|
|
|
if self.app:
|
2017-05-24 04:24:22 +02:00
|
|
|
return self.app.qubesd_call(dest, method, arg, payload,
|
|
|
|
payload_stream)
|
2017-02-24 01:21:02 +01:00
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _parse_qubesd_response(response_data):
|
2017-02-25 20:56:31 +01:00
|
|
|
'''Parse response from qubesd.
|
|
|
|
|
|
|
|
In case of success, return actual data. In case of error,
|
|
|
|
raise appropriate exception.
|
|
|
|
'''
|
2017-03-11 01:41:58 +01:00
|
|
|
|
2017-04-21 02:47:23 +02:00
|
|
|
if response_data == b'':
|
2017-05-11 23:21:04 +02:00
|
|
|
raise qubesadmin.exc.QubesDaemonNoResponseError(
|
2017-08-12 15:15:52 +02:00
|
|
|
'Got empty response from qubesd. See journalctl in dom0 for '
|
|
|
|
'details.')
|
2017-03-11 01:41:58 +01:00
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
if response_data[0:2] == b'\x30\x00':
|
|
|
|
return response_data[2:]
|
2018-07-16 02:25:25 +02:00
|
|
|
if response_data[0:2] == b'\x32\x00':
|
2017-02-24 01:21:02 +01:00
|
|
|
(_, exc_type, _traceback, format_string, args) = \
|
|
|
|
response_data.split(b'\x00', 4)
|
|
|
|
# drop last field because of terminating '\x00'
|
|
|
|
args = [arg.decode() for arg in args.split(b'\x00')[:-1]]
|
|
|
|
format_string = format_string.decode('utf-8')
|
|
|
|
exc_type = exc_type.decode('ascii')
|
2017-03-09 01:33:31 +01:00
|
|
|
try:
|
2017-05-11 23:21:04 +02:00
|
|
|
exc_class = getattr(qubesadmin.exc, exc_type)
|
2017-03-09 01:33:31 +01:00
|
|
|
except AttributeError:
|
|
|
|
if exc_type.endswith('Error'):
|
|
|
|
exc_class = __builtins__.get(exc_type,
|
2017-05-11 23:21:04 +02:00
|
|
|
qubesadmin.exc.QubesException)
|
2017-03-09 01:33:31 +01:00
|
|
|
else:
|
2017-05-11 23:21:04 +02:00
|
|
|
exc_class = qubesadmin.exc.QubesException
|
2017-02-24 01:21:02 +01:00
|
|
|
# TODO: handle traceback if given
|
|
|
|
raise exc_class(format_string, *args)
|
2019-03-07 03:07:42 +01:00
|
|
|
raise qubesadmin.exc.QubesDaemonCommunicationError(
|
|
|
|
'Invalid response format')
|
2017-02-24 01:21:02 +01:00
|
|
|
|
|
|
|
def property_list(self):
|
|
|
|
'''
|
|
|
|
List available properties (their names).
|
|
|
|
|
|
|
|
:return: list of strings
|
|
|
|
'''
|
|
|
|
if self._properties is None:
|
|
|
|
properties_str = self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'List',
|
|
|
|
None,
|
|
|
|
None)
|
|
|
|
self._properties = properties_str.decode('ascii').splitlines()
|
|
|
|
# TODO: make it somehow immutable
|
|
|
|
return self._properties
|
|
|
|
|
2017-03-11 01:45:57 +01:00
|
|
|
def property_help(self, name):
|
|
|
|
'''
|
|
|
|
Get description of a property.
|
|
|
|
|
|
|
|
:return: property help text
|
|
|
|
'''
|
|
|
|
help_text = self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Help',
|
|
|
|
name,
|
|
|
|
None)
|
|
|
|
return help_text.decode('ascii')
|
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
def property_is_default(self, item):
|
|
|
|
'''
|
|
|
|
Check if given property have default value
|
|
|
|
|
|
|
|
:param str item: name of property
|
|
|
|
:return: bool
|
|
|
|
'''
|
|
|
|
if item.startswith('_'):
|
|
|
|
raise AttributeError(item)
|
|
|
|
property_str = self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Get',
|
|
|
|
item,
|
|
|
|
None)
|
|
|
|
(default, _value) = property_str.split(b' ', 1)
|
|
|
|
assert default.startswith(b'default=')
|
|
|
|
is_default_str = default.split(b'=')[1]
|
2017-11-09 16:11:22 +01:00
|
|
|
is_default = is_default_str.decode('ascii') == "True"
|
2017-02-24 01:21:02 +01:00
|
|
|
assert isinstance(is_default, bool)
|
|
|
|
return is_default
|
|
|
|
|
2018-01-12 23:33:32 +01:00
|
|
|
def property_get_default(self, item):
|
|
|
|
'''
|
|
|
|
Get default property value, regardless of the current value
|
|
|
|
|
|
|
|
:param str item: name of property
|
|
|
|
:return: default value
|
|
|
|
'''
|
|
|
|
if item.startswith('_'):
|
|
|
|
raise AttributeError(item)
|
|
|
|
property_str = self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'GetDefault',
|
|
|
|
item,
|
|
|
|
None)
|
|
|
|
if not property_str:
|
|
|
|
raise AttributeError(item + ' has no default')
|
|
|
|
(prop_type, value) = property_str.split(b' ', 1)
|
|
|
|
return self._parse_type_value(prop_type, value)
|
|
|
|
|
2017-06-19 23:59:49 +02:00
|
|
|
def clone_properties(self, src, proplist=None):
|
|
|
|
'''Clone properties from other object.
|
|
|
|
|
|
|
|
:param PropertyHolder src: source object
|
|
|
|
:param list proplist: list of properties \
|
|
|
|
(:py:obj:`None` or omit for all properties)
|
|
|
|
'''
|
|
|
|
|
|
|
|
if proplist is None:
|
|
|
|
proplist = self.property_list()
|
|
|
|
|
|
|
|
for prop in proplist:
|
|
|
|
try:
|
|
|
|
setattr(self, prop, getattr(src, prop))
|
|
|
|
except AttributeError:
|
|
|
|
continue
|
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
def __getattr__(self, item):
|
|
|
|
if item.startswith('_'):
|
|
|
|
raise AttributeError(item)
|
2017-03-11 01:44:06 +01:00
|
|
|
try:
|
|
|
|
property_str = self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Get',
|
|
|
|
item,
|
|
|
|
None)
|
2017-05-11 23:21:04 +02:00
|
|
|
except qubesadmin.exc.QubesDaemonNoResponseError:
|
|
|
|
raise qubesadmin.exc.QubesPropertyAccessError(item)
|
2017-03-11 19:03:09 +01:00
|
|
|
(_default, prop_type, value) = property_str.split(b' ', 2)
|
2018-01-12 23:33:32 +01:00
|
|
|
return self._parse_type_value(prop_type, value)
|
|
|
|
|
|
|
|
def _parse_type_value(self, prop_type, value):
|
|
|
|
'''
|
|
|
|
Parse `type=... ...` qubesd response format. Return a value of
|
|
|
|
appropriate type.
|
|
|
|
|
|
|
|
:param bytes prop_type: 'type=...' part of the response (including
|
|
|
|
`type=` prefix)
|
|
|
|
:param bytes value: 'value' part of the response
|
|
|
|
:return: parsed value
|
|
|
|
'''
|
|
|
|
# pylint: disable=too-many-return-statements
|
2017-03-11 19:03:09 +01:00
|
|
|
prop_type = prop_type.decode('ascii')
|
|
|
|
if not prop_type.startswith('type='):
|
2017-05-11 23:21:04 +02:00
|
|
|
raise qubesadmin.exc.QubesDaemonCommunicationError(
|
2017-03-11 19:03:09 +01:00
|
|
|
'Invalid type prefix received: {}'.format(prop_type))
|
|
|
|
(_, prop_type) = prop_type.split('=', 1)
|
2017-02-24 01:21:02 +01:00
|
|
|
value = value.decode()
|
2017-03-11 19:03:09 +01:00
|
|
|
if prop_type == 'str':
|
|
|
|
return str(value)
|
2018-07-16 02:25:25 +02:00
|
|
|
if prop_type == 'bool':
|
2017-03-11 19:03:09 +01:00
|
|
|
if value == '':
|
|
|
|
raise AttributeError
|
2017-11-09 16:11:22 +01:00
|
|
|
return value == "True"
|
2018-07-16 02:25:25 +02:00
|
|
|
if prop_type == 'int':
|
2017-03-11 19:03:09 +01:00
|
|
|
if value == '':
|
|
|
|
raise AttributeError
|
2017-11-09 16:11:22 +01:00
|
|
|
return int(value)
|
2018-07-16 02:25:25 +02:00
|
|
|
if prop_type == 'vm':
|
2017-03-11 19:03:09 +01:00
|
|
|
if value == '':
|
|
|
|
return None
|
2019-06-14 17:10:21 +02:00
|
|
|
return self.app.domains.get_blind(value)
|
2018-07-16 02:25:25 +02:00
|
|
|
if prop_type == 'label':
|
2017-03-11 19:03:09 +01:00
|
|
|
if value == '':
|
|
|
|
return None
|
2017-11-09 15:54:29 +01:00
|
|
|
return self.app.labels.get_blind(value)
|
2018-07-16 02:25:25 +02:00
|
|
|
raise qubesadmin.exc.QubesDaemonCommunicationError(
|
|
|
|
'Received invalid value type: {}'.format(prop_type))
|
2017-02-24 01:21:02 +01:00
|
|
|
|
2017-11-09 19:50:48 +01:00
|
|
|
@classmethod
|
|
|
|
def _local_properties(cls):
|
|
|
|
'''
|
|
|
|
Get set of property names that are properties on the Python object,
|
|
|
|
and must not be set on the remote object
|
|
|
|
'''
|
|
|
|
if "_local_properties_set" not in cls.__dict__:
|
|
|
|
props = set()
|
|
|
|
for class_ in cls.__mro__:
|
|
|
|
for key in class_.__dict__:
|
|
|
|
props.add(key)
|
|
|
|
cls._local_properties_set = props
|
|
|
|
|
|
|
|
return cls._local_properties_set
|
|
|
|
|
2017-02-24 01:21:02 +01:00
|
|
|
def __setattr__(self, key, value):
|
2017-11-09 19:50:48 +01:00
|
|
|
if key.startswith('_') or key in self._local_properties():
|
2017-02-24 01:21:02 +01:00
|
|
|
return super(PropertyHolder, self).__setattr__(key, value)
|
2017-05-11 23:21:04 +02:00
|
|
|
if value is qubesadmin.DEFAULT:
|
2017-03-11 01:44:06 +01:00
|
|
|
try:
|
|
|
|
self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Reset',
|
|
|
|
key,
|
|
|
|
None)
|
2017-05-11 23:21:04 +02:00
|
|
|
except qubesadmin.exc.QubesDaemonNoResponseError:
|
|
|
|
raise qubesadmin.exc.QubesPropertyAccessError(key)
|
2017-02-24 01:21:02 +01:00
|
|
|
else:
|
2017-05-11 23:21:04 +02:00
|
|
|
if isinstance(value, qubesadmin.vm.QubesVM):
|
2017-02-24 01:38:47 +01:00
|
|
|
value = value.name
|
2017-05-25 12:30:19 +02:00
|
|
|
if value is None:
|
|
|
|
value = ''
|
2017-03-11 01:44:06 +01:00
|
|
|
try:
|
|
|
|
self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Set',
|
|
|
|
key,
|
|
|
|
str(value).encode('utf-8'))
|
2017-05-11 23:21:04 +02:00
|
|
|
except qubesadmin.exc.QubesDaemonNoResponseError:
|
|
|
|
raise qubesadmin.exc.QubesPropertyAccessError(key)
|
2017-02-24 01:21:02 +01:00
|
|
|
|
|
|
|
def __delattr__(self, name):
|
2017-11-09 19:50:48 +01:00
|
|
|
if name.startswith('_') or name in self._local_properties():
|
2017-02-24 01:21:02 +01:00
|
|
|
return super(PropertyHolder, self).__delattr__(name)
|
2017-03-11 01:44:06 +01:00
|
|
|
try:
|
|
|
|
self.qubesd_call(
|
|
|
|
self._method_dest,
|
|
|
|
self._method_prefix + 'Reset',
|
|
|
|
name
|
|
|
|
)
|
2017-05-11 23:21:04 +02:00
|
|
|
except qubesadmin.exc.QubesDaemonNoResponseError:
|
|
|
|
raise qubesadmin.exc.QubesPropertyAccessError(name)
|
2017-03-12 20:23:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
class WrapperObjectsCollection(object):
|
|
|
|
'''Collection of simple named objects'''
|
|
|
|
def __init__(self, app, list_method, object_class):
|
|
|
|
'''
|
|
|
|
Construct manager of named wrapper objects.
|
|
|
|
|
|
|
|
:param app: Qubes() object
|
|
|
|
:param list_method: name of API method used to list objects,
|
|
|
|
must return simple "one name per line" list
|
|
|
|
:param object_class: object class (callable) for wrapper objects,
|
|
|
|
will be called with just two arguments: app and a name
|
|
|
|
'''
|
|
|
|
self.app = app
|
|
|
|
self._list_method = list_method
|
|
|
|
self._object_class = object_class
|
|
|
|
#: names cache
|
|
|
|
self._names_list = None
|
|
|
|
#: returned objects cache
|
|
|
|
self._objects = {}
|
|
|
|
|
|
|
|
def clear_cache(self):
|
|
|
|
'''Clear cached list of names'''
|
|
|
|
self._names_list = None
|
|
|
|
|
|
|
|
def refresh_cache(self, force=False):
|
|
|
|
'''Refresh cached list of names'''
|
|
|
|
if not force and self._names_list is not None:
|
|
|
|
return
|
|
|
|
list_data = self.app.qubesd_call('dom0', self._list_method)
|
|
|
|
list_data = list_data.decode('ascii')
|
|
|
|
assert list_data[-1] == '\n'
|
2017-03-13 04:28:13 +01:00
|
|
|
self._names_list = [str(name) for name in list_data[:-1].splitlines()]
|
2017-03-12 20:23:07 +01:00
|
|
|
|
|
|
|
for name, obj in list(self._objects.items()):
|
|
|
|
if obj.name not in self._names_list:
|
|
|
|
# Object no longer exists
|
|
|
|
del self._objects[name]
|
|
|
|
|
|
|
|
def __getitem__(self, item):
|
2017-09-20 19:12:24 +02:00
|
|
|
if not self.app.blind_mode and item not in self:
|
2017-03-12 20:23:07 +01:00
|
|
|
raise KeyError(item)
|
2017-11-09 15:54:29 +01:00
|
|
|
return self.get_blind(item)
|
|
|
|
|
|
|
|
def get_blind(self, item):
|
|
|
|
'''
|
|
|
|
Get a property without downloading the list
|
|
|
|
and checking if it's present
|
|
|
|
'''
|
2017-03-12 20:23:07 +01:00
|
|
|
if item not in self._objects:
|
|
|
|
self._objects[item] = self._object_class(self.app, item)
|
|
|
|
return self._objects[item]
|
|
|
|
|
|
|
|
def __contains__(self, item):
|
|
|
|
self.refresh_cache()
|
|
|
|
return item in self._names_list
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
self.refresh_cache()
|
2017-12-23 01:33:26 +01:00
|
|
|
for key in self._names_list:
|
|
|
|
yield key
|
2017-03-12 20:23:07 +01:00
|
|
|
|
|
|
|
def keys(self):
|
|
|
|
'''Get list of names.'''
|
|
|
|
self.refresh_cache()
|
2019-10-05 21:57:48 +02:00
|
|
|
return list(self._names_list)
|
2017-12-23 01:33:26 +01:00
|
|
|
|
|
|
|
def items(self):
|
|
|
|
'''Get list of (key, value) pairs'''
|
|
|
|
self.refresh_cache()
|
|
|
|
return [(key, self.get_blind(key)) for key in self._names_list]
|
|
|
|
|
|
|
|
def values(self):
|
|
|
|
'''Get list of objects'''
|
|
|
|
self.refresh_cache()
|
|
|
|
return [self.get_blind(key) for key in self._names_list]
|