base.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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. self._properties_cache = {}
  41. def qubesd_call(self, dest, method, arg=None, payload=None,
  42. payload_stream=None):
  43. '''
  44. Call into qubesd using appropriate mechanism. This method should be
  45. defined by a subclass.
  46. Only one of `payload` and `payload_stream` can be specified.
  47. :param dest: Destination VM name
  48. :param method: Full API method name ('admin...')
  49. :param arg: Method argument (if any)
  50. :param payload: Payload send to the method
  51. :param payload_stream: file-like object to read payload from
  52. :return: Data returned by qubesd (string)
  53. '''
  54. if dest is None:
  55. dest = self._method_dest
  56. # have the actual implementation at Qubes() instance
  57. if self.app:
  58. return self.app.qubesd_call(dest, method, arg, payload,
  59. payload_stream)
  60. raise NotImplementedError
  61. @staticmethod
  62. def _parse_qubesd_response(response_data):
  63. '''Parse response from qubesd.
  64. In case of success, return actual data. In case of error,
  65. raise appropriate exception.
  66. '''
  67. if response_data == b'':
  68. raise qubesadmin.exc.QubesDaemonAccessError(
  69. 'Got empty response from qubesd. See journalctl in dom0 for '
  70. 'details.')
  71. if response_data[0:2] == b'\x30\x00':
  72. return response_data[2:]
  73. if response_data[0:2] == b'\x32\x00':
  74. (_, exc_type, _traceback, format_string, args) = \
  75. response_data.split(b'\x00', 4)
  76. # drop last field because of terminating '\x00'
  77. args = [arg.decode() for arg in args.split(b'\x00')[:-1]]
  78. format_string = format_string.decode('utf-8')
  79. exc_type = exc_type.decode('ascii')
  80. try:
  81. exc_class = getattr(qubesadmin.exc, exc_type)
  82. except AttributeError:
  83. if exc_type.endswith('Error'):
  84. exc_class = __builtins__.get(exc_type,
  85. qubesadmin.exc.QubesException)
  86. else:
  87. exc_class = qubesadmin.exc.QubesException
  88. # TODO: handle traceback if given
  89. raise exc_class(format_string, *args)
  90. raise qubesadmin.exc.QubesDaemonCommunicationError(
  91. 'Invalid response format')
  92. def property_list(self):
  93. '''
  94. List available properties (their names).
  95. :return: list of strings
  96. '''
  97. if self._properties is None:
  98. properties_str = self.qubesd_call(
  99. self._method_dest,
  100. self._method_prefix + 'List',
  101. None,
  102. None)
  103. self._properties = properties_str.decode('ascii').splitlines()
  104. # TODO: make it somehow immutable
  105. return self._properties
  106. def property_help(self, name):
  107. '''
  108. Get description of a property.
  109. :return: property help text
  110. '''
  111. help_text = self.qubesd_call(
  112. self._method_dest,
  113. self._method_prefix + 'Help',
  114. name,
  115. None)
  116. return help_text.decode('ascii')
  117. def property_is_default(self, item):
  118. '''
  119. Check if given property have default value
  120. :param str item: name of property
  121. :return: bool
  122. '''
  123. if item.startswith('_'):
  124. raise AttributeError(item)
  125. # pre-fill cache if enabled
  126. if self.app.cache_enabled and not self._properties_cache:
  127. self._fetch_all_properties()
  128. # cached value
  129. if item in self._properties_cache:
  130. return self._properties_cache[item][0]
  131. # cached properties list
  132. if self._properties is not None and item not in self._properties:
  133. raise AttributeError(item)
  134. try:
  135. property_str = self.qubesd_call(
  136. self._method_dest,
  137. self._method_prefix + 'Get',
  138. item,
  139. None)
  140. except (qubesadmin.exc.QubesDaemonAccessError,
  141. qubesadmin.exc.QubesVMNotFoundError):
  142. raise qubesadmin.exc.QubesPropertyAccessError(item)
  143. is_default, value = self._deserialize_property(property_str)
  144. if self.app.cache_enabled:
  145. self._properties_cache[item] = (is_default, value)
  146. return is_default
  147. def property_get_default(self, item):
  148. '''
  149. Get default property value, regardless of the current value
  150. :param str item: name of property
  151. :return: default value
  152. '''
  153. if item.startswith('_'):
  154. raise AttributeError(item)
  155. try:
  156. property_str = self.qubesd_call(
  157. self._method_dest,
  158. self._method_prefix + 'GetDefault',
  159. item,
  160. None)
  161. except (qubesadmin.exc.QubesDaemonAccessError,
  162. qubesadmin.exc.QubesVMNotFoundError):
  163. raise qubesadmin.exc.QubesPropertyAccessError(item)
  164. if not property_str:
  165. raise AttributeError(item + ' has no default')
  166. (prop_type, value) = property_str.split(b' ', 1)
  167. return self._parse_type_value(prop_type, value)
  168. def clone_properties(self, src, proplist=None):
  169. '''Clone properties from other object.
  170. :param PropertyHolder src: source object
  171. :param list proplist: list of properties \
  172. (:py:obj:`None` or omit for all properties)
  173. '''
  174. if proplist is None:
  175. proplist = self.property_list()
  176. for prop in proplist:
  177. try:
  178. setattr(self, prop, getattr(src, prop))
  179. except AttributeError:
  180. continue
  181. def __getattr__(self, item):
  182. if item.startswith('_'):
  183. raise AttributeError(item)
  184. # pre-fill cache if enabled
  185. if self.app.cache_enabled and not self._properties_cache:
  186. self._fetch_all_properties()
  187. # cached value
  188. if item in self._properties_cache:
  189. value = self._properties_cache[item][1]
  190. if value is AttributeError:
  191. raise AttributeError(item)
  192. return value
  193. # cached properties list
  194. if self._properties is not None and item not in self._properties:
  195. raise AttributeError(item)
  196. try:
  197. property_str = self.qubesd_call(
  198. self._method_dest,
  199. self._method_prefix + 'Get',
  200. item,
  201. None)
  202. except (qubesadmin.exc.QubesDaemonNoResponseError,
  203. qubesadmin.exc.QubesVMNotFoundError):
  204. raise qubesadmin.exc.QubesPropertyAccessError(item)
  205. is_default, value = self._deserialize_property(property_str)
  206. if self.app.cache_enabled:
  207. self._properties_cache[item] = (is_default, value)
  208. if value is AttributeError:
  209. raise AttributeError(item)
  210. return value
  211. def _deserialize_property(self, api_response):
  212. """
  213. Deserialize property.Get response format
  214. :param api_response: bytes, as retrieved from qubesd
  215. :return: tuple(is_default, value)
  216. """
  217. (default, prop_type, value) = api_response.split(b' ', 2)
  218. assert default.startswith(b'default=')
  219. is_default_str = default.split(b'=')[1]
  220. is_default = is_default_str.decode('ascii') == "True"
  221. value = self._parse_type_value(prop_type, value)
  222. return is_default, value
  223. def _parse_type_value(self, prop_type, value):
  224. '''
  225. Parse `type=... ...` qubesd response format. Return a value of
  226. appropriate type.
  227. :param bytes prop_type: 'type=...' part of the response (including
  228. `type=` prefix)
  229. :param bytes value: 'value' part of the response
  230. :return: parsed value
  231. '''
  232. # pylint: disable=too-many-return-statements
  233. prop_type = prop_type.decode('ascii')
  234. if not prop_type.startswith('type='):
  235. raise qubesadmin.exc.QubesDaemonCommunicationError(
  236. 'Invalid type prefix received: {}'.format(prop_type))
  237. (_, prop_type) = prop_type.split('=', 1)
  238. value = value.decode()
  239. if prop_type == 'str':
  240. return str(value)
  241. if prop_type == 'bool':
  242. if value == '':
  243. return AttributeError
  244. return value == "True"
  245. if prop_type == 'int':
  246. if value == '':
  247. return AttributeError
  248. return int(value)
  249. if prop_type == 'vm':
  250. if value == '':
  251. return None
  252. return self.app.domains.get_blind(value)
  253. if prop_type == 'label':
  254. if value == '':
  255. return None
  256. return self.app.labels.get_blind(value)
  257. raise qubesadmin.exc.QubesDaemonCommunicationError(
  258. 'Received invalid value type: {}'.format(prop_type))
  259. def _fetch_all_properties(self):
  260. """
  261. Retrieve all properties values at once using (prefix).property.GetAll
  262. method. If it succeed, save retrieved values in the properties cache.
  263. If the request fails (for example because of qrexec policy), do nothing.
  264. Exceptions when parsing received value are not handled.
  265. :return: None
  266. """
  267. def unescape(line):
  268. """Handle \\-escaped values, generates a list of character codes"""
  269. escaped = False
  270. for char in line:
  271. if escaped:
  272. assert char in (ord('n'), ord('\\'))
  273. if char == ord('n'):
  274. yield ord('\n')
  275. elif char == ord('\\'):
  276. yield char
  277. escaped = False
  278. elif char == ord('\\'):
  279. escaped = True
  280. else:
  281. yield char
  282. assert not escaped
  283. try:
  284. properties_str = self.qubesd_call(
  285. self._method_dest,
  286. self._method_prefix + 'GetAll',
  287. None,
  288. None)
  289. except qubesadmin.exc.QubesDaemonNoResponseError:
  290. return
  291. for line in properties_str.splitlines():
  292. # decode newlines
  293. line = bytes(unescape(line))
  294. name, property_str = line.split(b' ', 1)
  295. name = name.decode()
  296. is_default, value = self._deserialize_property(property_str)
  297. self._properties_cache[name] = (is_default, value)
  298. self._properties = list(self._properties_cache.keys())
  299. @classmethod
  300. def _local_properties(cls):
  301. '''
  302. Get set of property names that are properties on the Python object,
  303. and must not be set on the remote object
  304. '''
  305. if "_local_properties_set" not in cls.__dict__:
  306. props = set()
  307. for class_ in cls.__mro__:
  308. for key in class_.__dict__:
  309. props.add(key)
  310. cls._local_properties_set = props
  311. return cls._local_properties_set
  312. def __setattr__(self, key, value):
  313. if key.startswith('_') or key in self._local_properties():
  314. return super().__setattr__(key, value)
  315. if value is qubesadmin.DEFAULT:
  316. try:
  317. self.qubesd_call(
  318. self._method_dest,
  319. self._method_prefix + 'Reset',
  320. key,
  321. None)
  322. except (qubesadmin.exc.QubesDaemonNoResponseError,
  323. qubesadmin.exc.QubesVMNotFoundError):
  324. raise qubesadmin.exc.QubesPropertyAccessError(key)
  325. else:
  326. if isinstance(value, qubesadmin.vm.QubesVM):
  327. value = value.name
  328. if value is None:
  329. value = ''
  330. try:
  331. self.qubesd_call(
  332. self._method_dest,
  333. self._method_prefix + 'Set',
  334. key,
  335. str(value).encode('utf-8'))
  336. except (qubesadmin.exc.QubesDaemonNoResponseError,
  337. qubesadmin.exc.QubesVMNotFoundError):
  338. raise qubesadmin.exc.QubesPropertyAccessError(key)
  339. def __delattr__(self, name):
  340. if name.startswith('_') or name in self._local_properties():
  341. return super().__delattr__(name)
  342. try:
  343. self.qubesd_call(
  344. self._method_dest,
  345. self._method_prefix + 'Reset',
  346. name
  347. )
  348. except (qubesadmin.exc.QubesDaemonNoResponseError,
  349. qubesadmin.exc.QubesVMNotFoundError):
  350. raise qubesadmin.exc.QubesPropertyAccessError(name)
  351. class WrapperObjectsCollection(object):
  352. '''Collection of simple named objects'''
  353. def __init__(self, app, list_method, object_class):
  354. '''
  355. Construct manager of named wrapper objects.
  356. :param app: Qubes() object
  357. :param list_method: name of API method used to list objects,
  358. must return simple "one name per line" list
  359. :param object_class: object class (callable) for wrapper objects,
  360. will be called with just two arguments: app and a name
  361. '''
  362. self.app = app
  363. self._list_method = list_method
  364. self._object_class = object_class
  365. #: names cache
  366. self._names_list = None
  367. #: returned objects cache
  368. self._objects = {}
  369. def clear_cache(self):
  370. '''Clear cached list of names'''
  371. self._names_list = None
  372. def refresh_cache(self, force=False):
  373. '''Refresh cached list of names'''
  374. if not force and self._names_list is not None:
  375. return
  376. list_data = self.app.qubesd_call('dom0', self._list_method)
  377. list_data = list_data.decode('ascii')
  378. assert list_data[-1] == '\n'
  379. self._names_list = [str(name) for name in list_data[:-1].splitlines()]
  380. for name, obj in list(self._objects.items()):
  381. if obj.name not in self._names_list:
  382. # Object no longer exists
  383. del self._objects[name]
  384. def __getitem__(self, item):
  385. if not self.app.blind_mode and item not in self:
  386. raise KeyError(item)
  387. return self.get_blind(item)
  388. def get_blind(self, item):
  389. '''
  390. Get a property without downloading the list
  391. and checking if it's present
  392. '''
  393. if item not in self._objects:
  394. self._objects[item] = self._object_class(self.app, item)
  395. return self._objects[item]
  396. def __contains__(self, item):
  397. self.refresh_cache()
  398. return item in self._names_list
  399. def __iter__(self):
  400. self.refresh_cache()
  401. for key in self._names_list:
  402. yield key
  403. def keys(self):
  404. '''Get list of names.'''
  405. self.refresh_cache()
  406. return list(self._names_list)
  407. def items(self):
  408. '''Get list of (key, value) pairs'''
  409. self.refresh_cache()
  410. return [(key, self.get_blind(key)) for key in self._names_list]
  411. def values(self):
  412. '''Get list of objects'''
  413. self.refresh_cache()
  414. return [self.get_blind(key) for key in self._names_list]