base.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. '''Base classes for managed objects'''
  21. import qubesadmin.exc
  22. DEFAULT = object()
  23. class PropertyHolder(object):
  24. '''A base class for object having properties retrievable using mgmt API.
  25. Warning: each (non-private) local attribute needs to be defined at class
  26. level, even if initialized in __init__; otherwise will be treated as
  27. property retrievable using mgmt call.
  28. '''
  29. #: a place for appropriate Qubes() object (QubesLocal or QubesRemote),
  30. # use None for self
  31. app = None
  32. def __init__(self, app, method_prefix, method_dest):
  33. #: appropriate Qubes() object (QubesLocal or QubesRemote), use None
  34. # for self
  35. self.app = app
  36. self._method_prefix = method_prefix
  37. self._method_dest = method_dest
  38. self._properties = None
  39. self._properties_help = None
  40. def qubesd_call(self, dest, method, arg=None, payload=None,
  41. payload_stream=None):
  42. '''
  43. Call into qubesd using appropriate mechanism. This method should be
  44. defined by a subclass.
  45. Only one of `payload` and `payload_stream` can be specified.
  46. :param dest: Destination VM name
  47. :param method: Full API method name ('admin...')
  48. :param arg: Method argument (if any)
  49. :param payload: Payload send to the method
  50. :param payload_stream: file-like object to read payload from
  51. :return: Data returned by qubesd (string)
  52. '''
  53. if dest is None:
  54. dest = self._method_dest
  55. # have the actual implementation at Qubes() instance
  56. if self.app:
  57. return self.app.qubesd_call(dest, method, arg, payload,
  58. payload_stream)
  59. raise NotImplementedError
  60. @staticmethod
  61. def _parse_qubesd_response(response_data):
  62. '''Parse response from qubesd.
  63. In case of success, return actual data. In case of error,
  64. raise appropriate exception.
  65. '''
  66. if response_data == b'':
  67. raise qubesadmin.exc.QubesDaemonNoResponseError(
  68. 'Got empty response from qubesd. See journalctl in dom0 for '
  69. 'details.')
  70. if response_data[0:2] == b'\x30\x00':
  71. return response_data[2:]
  72. if response_data[0:2] == b'\x32\x00':
  73. (_, exc_type, _traceback, format_string, args) = \
  74. response_data.split(b'\x00', 4)
  75. # drop last field because of terminating '\x00'
  76. args = [arg.decode() for arg in args.split(b'\x00')[:-1]]
  77. format_string = format_string.decode('utf-8')
  78. exc_type = exc_type.decode('ascii')
  79. try:
  80. exc_class = getattr(qubesadmin.exc, exc_type)
  81. except AttributeError:
  82. if exc_type.endswith('Error'):
  83. exc_class = __builtins__.get(exc_type,
  84. qubesadmin.exc.QubesException)
  85. else:
  86. exc_class = qubesadmin.exc.QubesException
  87. # TODO: handle traceback if given
  88. raise exc_class(format_string, *args)
  89. raise qubesadmin.exc.QubesDaemonCommunicationError(
  90. 'Invalid response format')
  91. def property_list(self):
  92. '''
  93. List available properties (their names).
  94. :return: list of strings
  95. '''
  96. if self._properties is None:
  97. properties_str = self.qubesd_call(
  98. self._method_dest,
  99. self._method_prefix + 'List',
  100. None,
  101. None)
  102. self._properties = properties_str.decode('ascii').splitlines()
  103. # TODO: make it somehow immutable
  104. return self._properties
  105. def property_help(self, name):
  106. '''
  107. Get description of a property.
  108. :return: property help text
  109. '''
  110. help_text = self.qubesd_call(
  111. self._method_dest,
  112. self._method_prefix + 'Help',
  113. name,
  114. None)
  115. return help_text.decode('ascii')
  116. def property_is_default(self, item):
  117. '''
  118. Check if given property have default value
  119. :param str item: name of property
  120. :return: bool
  121. '''
  122. if item.startswith('_'):
  123. raise AttributeError(item)
  124. property_str = self.qubesd_call(
  125. self._method_dest,
  126. self._method_prefix + 'Get',
  127. item,
  128. None)
  129. (default, _value) = property_str.split(b' ', 1)
  130. assert default.startswith(b'default=')
  131. is_default_str = default.split(b'=')[1]
  132. is_default = is_default_str.decode('ascii') == "True"
  133. assert isinstance(is_default, bool)
  134. return is_default
  135. def property_get_default(self, item):
  136. '''
  137. Get default property value, regardless of the current value
  138. :param str item: name of property
  139. :return: default value
  140. '''
  141. if item.startswith('_'):
  142. raise AttributeError(item)
  143. property_str = self.qubesd_call(
  144. self._method_dest,
  145. self._method_prefix + 'GetDefault',
  146. item,
  147. None)
  148. if not property_str:
  149. raise AttributeError(item + ' has no default')
  150. (prop_type, value) = property_str.split(b' ', 1)
  151. return self._parse_type_value(prop_type, value)
  152. def clone_properties(self, src, proplist=None):
  153. '''Clone properties from other object.
  154. :param PropertyHolder src: source object
  155. :param list proplist: list of properties \
  156. (:py:obj:`None` or omit for all properties)
  157. '''
  158. if proplist is None:
  159. proplist = self.property_list()
  160. for prop in proplist:
  161. try:
  162. setattr(self, prop, getattr(src, prop))
  163. except AttributeError:
  164. continue
  165. def __getattr__(self, item):
  166. if item.startswith('_'):
  167. raise AttributeError(item)
  168. try:
  169. property_str = self.qubesd_call(
  170. self._method_dest,
  171. self._method_prefix + 'Get',
  172. item,
  173. None)
  174. except qubesadmin.exc.QubesDaemonNoResponseError:
  175. raise qubesadmin.exc.QubesPropertyAccessError(item)
  176. (_default, prop_type, value) = property_str.split(b' ', 2)
  177. return self._parse_type_value(prop_type, value)
  178. def _parse_type_value(self, prop_type, value):
  179. '''
  180. Parse `type=... ...` qubesd response format. Return a value of
  181. appropriate type.
  182. :param bytes prop_type: 'type=...' part of the response (including
  183. `type=` prefix)
  184. :param bytes value: 'value' part of the response
  185. :return: parsed value
  186. '''
  187. # pylint: disable=too-many-return-statements
  188. prop_type = prop_type.decode('ascii')
  189. if not prop_type.startswith('type='):
  190. raise qubesadmin.exc.QubesDaemonCommunicationError(
  191. 'Invalid type prefix received: {}'.format(prop_type))
  192. (_, prop_type) = prop_type.split('=', 1)
  193. value = value.decode()
  194. if prop_type == 'str':
  195. return str(value)
  196. if prop_type == 'bool':
  197. if value == '':
  198. raise AttributeError
  199. return value == "True"
  200. if prop_type == 'int':
  201. if value == '':
  202. raise AttributeError
  203. return int(value)
  204. if prop_type == 'vm':
  205. if value == '':
  206. return None
  207. return self.app.domains[value]
  208. if prop_type == 'label':
  209. if value == '':
  210. return None
  211. return self.app.labels.get_blind(value)
  212. raise qubesadmin.exc.QubesDaemonCommunicationError(
  213. 'Received invalid value type: {}'.format(prop_type))
  214. @classmethod
  215. def _local_properties(cls):
  216. '''
  217. Get set of property names that are properties on the Python object,
  218. and must not be set on the remote object
  219. '''
  220. if "_local_properties_set" not in cls.__dict__:
  221. props = set()
  222. for class_ in cls.__mro__:
  223. for key in class_.__dict__:
  224. props.add(key)
  225. cls._local_properties_set = props
  226. return cls._local_properties_set
  227. def __setattr__(self, key, value):
  228. if key.startswith('_') or key in self._local_properties():
  229. return super(PropertyHolder, self).__setattr__(key, value)
  230. if value is qubesadmin.DEFAULT:
  231. try:
  232. self.qubesd_call(
  233. self._method_dest,
  234. self._method_prefix + 'Reset',
  235. key,
  236. None)
  237. except qubesadmin.exc.QubesDaemonNoResponseError:
  238. raise qubesadmin.exc.QubesPropertyAccessError(key)
  239. else:
  240. if isinstance(value, qubesadmin.vm.QubesVM):
  241. value = value.name
  242. if value is None:
  243. value = ''
  244. try:
  245. self.qubesd_call(
  246. self._method_dest,
  247. self._method_prefix + 'Set',
  248. key,
  249. str(value).encode('utf-8'))
  250. except qubesadmin.exc.QubesDaemonNoResponseError:
  251. raise qubesadmin.exc.QubesPropertyAccessError(key)
  252. def __delattr__(self, name):
  253. if name.startswith('_') or name in self._local_properties():
  254. return super(PropertyHolder, self).__delattr__(name)
  255. try:
  256. self.qubesd_call(
  257. self._method_dest,
  258. self._method_prefix + 'Reset',
  259. name
  260. )
  261. except qubesadmin.exc.QubesDaemonNoResponseError:
  262. raise qubesadmin.exc.QubesPropertyAccessError(name)
  263. class WrapperObjectsCollection(object):
  264. '''Collection of simple named objects'''
  265. def __init__(self, app, list_method, object_class):
  266. '''
  267. Construct manager of named wrapper objects.
  268. :param app: Qubes() object
  269. :param list_method: name of API method used to list objects,
  270. must return simple "one name per line" list
  271. :param object_class: object class (callable) for wrapper objects,
  272. will be called with just two arguments: app and a name
  273. '''
  274. self.app = app
  275. self._list_method = list_method
  276. self._object_class = object_class
  277. #: names cache
  278. self._names_list = None
  279. #: returned objects cache
  280. self._objects = {}
  281. def clear_cache(self):
  282. '''Clear cached list of names'''
  283. self._names_list = None
  284. def refresh_cache(self, force=False):
  285. '''Refresh cached list of names'''
  286. if not force and self._names_list is not None:
  287. return
  288. list_data = self.app.qubesd_call('dom0', self._list_method)
  289. list_data = list_data.decode('ascii')
  290. assert list_data[-1] == '\n'
  291. self._names_list = [str(name) for name in list_data[:-1].splitlines()]
  292. for name, obj in list(self._objects.items()):
  293. if obj.name not in self._names_list:
  294. # Object no longer exists
  295. del self._objects[name]
  296. def __getitem__(self, item):
  297. if not self.app.blind_mode and item not in self:
  298. raise KeyError(item)
  299. return self.get_blind(item)
  300. def get_blind(self, item):
  301. '''
  302. Get a property without downloading the list
  303. and checking if it's present
  304. '''
  305. if item not in self._objects:
  306. self._objects[item] = self._object_class(self.app, item)
  307. return self._objects[item]
  308. def __contains__(self, item):
  309. self.refresh_cache()
  310. return item in self._names_list
  311. def __iter__(self):
  312. self.refresh_cache()
  313. for key in self._names_list:
  314. yield key
  315. def keys(self):
  316. '''Get list of names.'''
  317. self.refresh_cache()
  318. return list(self._names_list)
  319. def items(self):
  320. '''Get list of (key, value) pairs'''
  321. self.refresh_cache()
  322. return [(key, self.get_blind(key)) for key in self._names_list]
  323. def values(self):
  324. '''Get list of objects'''
  325. self.refresh_cache()
  326. return [self.get_blind(key) for key in self._names_list]