__init__.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. #!/usr/bin/python2 -O
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2010-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  7. # Copyright (C) 2011-2015 Marek Marczykowski-Górecki
  8. # <marmarek@invisiblethingslab.com>
  9. # Copyright (C) 2014-2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  10. #
  11. # This program is free software; you can redistribute it and/or modify
  12. # it under the terms of the GNU General Public License as published by
  13. # the Free Software Foundation; either version 2 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License along
  22. # with this program; if not, write to the Free Software Foundation, Inc.,
  23. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  24. #
  25. '''
  26. Qubes OS
  27. :copyright: © 2010-2015 Invisible Things Lab
  28. '''
  29. from __future__ import absolute_import
  30. import __builtin__
  31. import collections
  32. import os
  33. import os.path
  34. import lxml.etree
  35. import qubes.config
  36. import qubes.events
  37. import qubes.exc
  38. __author__ = 'Invisible Things Lab'
  39. __license__ = 'GPLv2 or later'
  40. __version__ = 'R3'
  41. class Label(object):
  42. '''Label definition for virtual machines
  43. Label specifies colour of the padlock displayed next to VM's name.
  44. When this is a :py:class:`qubes.vm.dispvm.DispVM`, padlock is overlayed
  45. with recycling pictogram.
  46. :param int index: numeric identificator of label
  47. :param str color: colour specification as in HTML (``#abcdef``)
  48. :param str name: label's name like "red" or "green"
  49. '''
  50. def __init__(self, index, color, name):
  51. #: numeric identificator of label
  52. self.index = index
  53. #: colour specification as in HTML (``#abcdef``)
  54. self.color = color
  55. #: label's name like "red" or "green"
  56. self.name = name
  57. #: freedesktop icon name, suitable for use in
  58. #: :py:meth:`PyQt4.QtGui.QIcon.fromTheme`
  59. self.icon = 'appvm-' + name
  60. #: freedesktop icon name, suitable for use in
  61. #: :py:meth:`PyQt4.QtGui.QIcon.fromTheme` on DispVMs
  62. self.icon_dispvm = 'dispvm-' + name
  63. @classmethod
  64. def fromxml(cls, xml):
  65. '''Create label definition from XML node
  66. :param lxml.etree._Element xml: XML node reference
  67. :rtype: :py:class:`qubes.Label`
  68. '''
  69. index = int(xml.get('id').split('-', 1)[1])
  70. color = xml.get('color')
  71. name = xml.text
  72. return cls(index, color, name)
  73. def __xml__(self):
  74. element = lxml.etree.Element(
  75. 'label', id='label-{}'.format(self.index), color=self.color)
  76. element.text = self.name
  77. return element
  78. def __str__(self):
  79. return self.name
  80. def __repr__(self):
  81. return '{}({!r}, {!r}, {!r})'.format(
  82. self.__class__.__name__,
  83. self.index,
  84. self.color,
  85. self.name)
  86. @__builtin__.property
  87. def icon_path(self):
  88. '''Icon path
  89. .. deprecated:: 2.0
  90. use :py:meth:`PyQt4.QtGui.QIcon.fromTheme` and :py:attr:`icon`
  91. '''
  92. return os.path.join(qubes.config.system_path['qubes_icon_dir'],
  93. self.icon) + ".png"
  94. @__builtin__.property
  95. def icon_path_dispvm(self):
  96. '''Icon path
  97. .. deprecated:: 2.0
  98. use :py:meth:`PyQt4.QtGui.QIcon.fromTheme` and :py:attr:`icon_dispvm`
  99. '''
  100. return os.path.join(qubes.config.system_path['qubes_icon_dir'],
  101. self.icon_dispvm) + ".png"
  102. class property(object): # pylint: disable=redefined-builtin,invalid-name
  103. '''Qubes property.
  104. This class holds one property that can be saved to and loaded from
  105. :file:`qubes.xml`. It is used for both global and per-VM properties.
  106. Property can be unset by ordinary ``del`` statement or assigning
  107. :py:attr:`DEFAULT` special value to it. After deletion (or before first
  108. assignment/load) attempting to read a property will get its default value
  109. or, when no default, py:class:`exceptions.AttributeError`.
  110. :param str name: name of the property
  111. :param collections.Callable setter: if not :py:obj:`None`, this is used to \
  112. initialise value; first parameter to the function is holder instance \
  113. and the second is value; this is called before ``type``
  114. :param collections.Callable saver: function to coerce value to something \
  115. readable by setter
  116. :param type type: if not :py:obj:`None`, value is coerced to this type
  117. :param object default: default value; if callable, will be called with \
  118. holder as first argument
  119. :param int load_stage: stage when property should be loaded (see \
  120. :py:class:`Qubes` for description of stages)
  121. :param int order: order of evaluation (bigger order values are later)
  122. :param bool clone: :py:meth:`PropertyHolder.clone_properties` will not \
  123. include this property by default if :py:obj:`False`
  124. :param str ls_head: column head for :program:`qvm-ls`
  125. :param int ls_width: column width in :program:`qvm-ls`
  126. :param str doc: docstring; this should be one paragraph of plain RST, no \
  127. sphinx-specific features
  128. Setters and savers have following signatures:
  129. .. :py:function:: setter(self, prop, value)
  130. :noindex:
  131. :param self: instance of object that is holding property
  132. :param prop: property object
  133. :param value: value being assigned
  134. .. :py:function:: saver(self, prop, value)
  135. :noindex:
  136. :param self: instance of object that is holding property
  137. :param prop: property object
  138. :param value: value being saved
  139. :rtype: str
  140. :raises property.DontSave: when property should not be saved at all
  141. '''
  142. #: Assigning this value to property means setting it to its default value.
  143. #: If property has no default value, this will unset it.
  144. DEFAULT = object()
  145. # internal use only
  146. _NO_DEFAULT = object()
  147. def __init__(self, name, setter=None, saver=None, type=None,
  148. default=_NO_DEFAULT, write_once=False, load_stage=2, order=0,
  149. save_via_ref=False, clone=True,
  150. ls_head=None, ls_width=None, doc=None):
  151. # pylint: disable=redefined-builtin
  152. self.__name__ = name
  153. self._setter = setter
  154. self._saver = saver if saver is not None else (
  155. lambda self, prop, value: str(value))
  156. self._type = type
  157. self._default = default
  158. self._write_once = write_once
  159. self.order = order
  160. self.load_stage = load_stage
  161. self.save_via_ref = save_via_ref
  162. self.clone = clone
  163. self.__doc__ = doc
  164. self._attr_name = '_qubesprop_' + name
  165. if ls_head is not None or ls_width is not None:
  166. self.ls_head = ls_head or self.__name__.replace('_', '-').upper()
  167. self.ls_width = max(ls_width or 0, len(self.ls_head) + 1)
  168. def __get__(self, instance, owner):
  169. if instance is None:
  170. return self
  171. # XXX this violates duck typing, shall we keep it?
  172. if not isinstance(instance, PropertyHolder):
  173. raise AttributeError('qubes.property should be used on '
  174. 'qubes.PropertyHolder instances only')
  175. try:
  176. return getattr(instance, self._attr_name)
  177. except AttributeError:
  178. if self._default is self._NO_DEFAULT:
  179. raise AttributeError(
  180. 'property {!r} not set'.format(self.__name__))
  181. elif isinstance(self._default, collections.Callable):
  182. return self._default(instance)
  183. else:
  184. return self._default
  185. def __set__(self, instance, value):
  186. self._enforce_write_once(instance)
  187. if value is self.__class__.DEFAULT:
  188. self.__delete__(instance)
  189. return
  190. try:
  191. oldvalue = getattr(instance, self.__name__)
  192. has_oldvalue = True
  193. except AttributeError:
  194. has_oldvalue = False
  195. if self._setter is not None:
  196. value = self._setter(instance, self, value)
  197. if self._type not in (None, type(value)):
  198. value = self._type(value)
  199. if has_oldvalue:
  200. instance.fire_event_pre('property-pre-set:' + self.__name__,
  201. self.__name__, value, oldvalue)
  202. else:
  203. instance.fire_event_pre('property-pre-set:' + self.__name__,
  204. self.__name__, value)
  205. instance._property_init(self, value) # pylint: disable=protected-access
  206. if has_oldvalue:
  207. instance.fire_event('property-set:' + self.__name__, self.__name__,
  208. value, oldvalue)
  209. else:
  210. instance.fire_event('property-set:' + self.__name__, self.__name__,
  211. value)
  212. def __delete__(self, instance):
  213. self._enforce_write_once(instance)
  214. try:
  215. oldvalue = getattr(instance, self.__name__)
  216. has_oldvalue = True
  217. except AttributeError:
  218. has_oldvalue = False
  219. if has_oldvalue:
  220. instance.fire_event_pre('property-pre-del:' + self.__name__,
  221. self.__name__, oldvalue)
  222. delattr(instance, self._attr_name)
  223. instance.fire_event('property-del:' + self.__name__,
  224. self.__name__, oldvalue)
  225. else:
  226. instance.fire_event_pre('property-pre-del:' + self.__name__,
  227. self.__name__)
  228. instance.fire_event('property-del:' + self.__name__,
  229. self.__name__)
  230. def __repr__(self):
  231. default = ' default={!r}'.format(self._default) \
  232. if self._default is not self._NO_DEFAULT \
  233. else ''
  234. return '<{} object at {:#x} name={!r}{}>'.format(
  235. self.__class__.__name__, id(self), self.__name__, default) \
  236. def __hash__(self):
  237. return hash(self.__name__)
  238. def __eq__(self, other):
  239. return isinstance(other, property) and self.__name__ == other.__name__
  240. def _enforce_write_once(self, instance):
  241. if self._write_once and not instance.property_is_default(self):
  242. raise AttributeError(
  243. 'property {!r} is write-once and already set'.format(
  244. self.__name__))
  245. #
  246. # exceptions
  247. #
  248. class DontSave(Exception):
  249. '''This exception may be raised from saver to sign that property should
  250. not be saved.
  251. '''
  252. pass
  253. @staticmethod
  254. def dontsave(self, prop, value):
  255. '''Dummy saver that never saves anything.'''
  256. # pylint: disable=bad-staticmethod-argument,unused-argument
  257. raise property.DontSave()
  258. #
  259. # some setters provided
  260. #
  261. @staticmethod
  262. def forbidden(self, prop, value):
  263. '''Property setter that forbids loading a property.
  264. This is used to effectively disable property in classes which inherit
  265. unwanted property. When someone attempts to load such a property, it
  266. :throws AttributeError: always
  267. ''' # pylint: disable=bad-staticmethod-argument,unused-argument
  268. raise AttributeError(
  269. 'setting {} property on {} instance is forbidden'.format(
  270. prop.__name__, self.__class__.__name__))
  271. @staticmethod
  272. def bool(self, prop, value):
  273. '''Property setter for boolean properties.
  274. It accepts (case-insensitive) ``'0'``, ``'no'`` and ``false`` as
  275. :py:obj:`False` and ``'1'``, ``'yes'`` and ``'true'`` as
  276. :py:obj:`True`.
  277. ''' # pylint: disable=bad-staticmethod-argument,unused-argument
  278. if isinstance(value, basestring):
  279. lcvalue = value.lower()
  280. if lcvalue in ('0', 'no', 'false', 'off'):
  281. return False
  282. if lcvalue in ('1', 'yes', 'true', 'on'):
  283. return True
  284. raise ValueError(
  285. 'Invalid literal for boolean property: {!r}'.format(value))
  286. return bool(value)
  287. class PropertyHolder(qubes.events.Emitter):
  288. '''Abstract class for holding :py:class:`qubes.property`
  289. Events fired by instances of this class:
  290. .. event:: property-load (subject, event)
  291. Fired once after all properties are loaded from XML. Individual
  292. ``property-set`` events are not fired.
  293. .. event:: property-set:<propname> \
  294. (subject, event, name, newvalue[, oldvalue])
  295. Fired when property changes state. Signature is variable,
  296. *oldvalue* is present only if there was an old value.
  297. :param name: Property name
  298. :param newvalue: New value of the property
  299. :param oldvalue: Old value of the property
  300. .. event:: property-pre-set:<propname> \
  301. (subject, event, name, newvalue[, oldvalue])
  302. Fired before property changes state. Signature is variable,
  303. *oldvalue* is present only if there was an old value.
  304. :param name: Property name
  305. :param newvalue: New value of the property
  306. :param oldvalue: Old value of the property
  307. .. event:: property-del:<propname> \
  308. (subject, event, name[, oldvalue])
  309. Fired when property gets deleted (is set to default). Signature is
  310. variable, *oldvalue* is present only if there was an old value.
  311. :param name: Property name
  312. :param oldvalue: Old value of the property
  313. .. event:: property-pre-del:<propname> \
  314. (subject, event, name[, oldvalue])
  315. Fired before property gets deleted (is set to default). Signature
  316. is variable, *oldvalue* is present only if there was an old value.
  317. :param name: Property name
  318. :param oldvalue: Old value of the property
  319. Members:
  320. '''
  321. def __init__(self, xml, **kwargs):
  322. self.xml = xml
  323. propvalues = {}
  324. all_names = set(prop.__name__ for prop in self.property_list())
  325. for key in list(kwargs):
  326. if not key in all_names:
  327. continue
  328. propvalues[key] = kwargs.pop(key)
  329. super(PropertyHolder, self).__init__(**kwargs)
  330. for key, value in propvalues.items():
  331. setattr(self, key, value)
  332. @classmethod
  333. def property_list(cls, load_stage=None):
  334. '''List all properties attached to this VM's class
  335. :param load_stage: Filter by load stage
  336. :type load_stage: :py:func:`int` or :py:obj:`None`
  337. '''
  338. props = set()
  339. for class_ in cls.__mro__:
  340. props.update(prop for prop in class_.__dict__.values()
  341. if isinstance(prop, property))
  342. if load_stage is not None:
  343. props = set(prop for prop in props
  344. if prop.load_stage == load_stage)
  345. return sorted(props,
  346. key=lambda prop: (prop.load_stage, prop.order, prop.__name__))
  347. def _property_init(self, prop, value):
  348. '''Initialise property to a given value, without side effects.
  349. :param qubes.property prop: property object of particular interest
  350. :param value: value
  351. '''
  352. # pylint: disable=protected-access
  353. setattr(self, self.property_get_def(prop)._attr_name, value)
  354. def property_is_default(self, prop):
  355. '''Check whether property is in it's default value.
  356. Properties when unset may return some default value, so
  357. ``hasattr(vm, prop.__name__)`` is wrong in some circumstances. This
  358. method allows for checking if the value returned is in fact it's
  359. default value.
  360. :param qubes.property prop: property object of particular interest
  361. :rtype: bool
  362. ''' # pylint: disable=protected-access
  363. # both property_get_def() and ._attr_name may throw AttributeError,
  364. # which we don't want to catch
  365. attrname = self.property_get_def(prop)._attr_name
  366. return not hasattr(self, attrname)
  367. @classmethod
  368. def property_get_def(cls, prop):
  369. '''Return property definition object.
  370. If prop is already :py:class:`qubes.property` instance, return the same
  371. object.
  372. :param prop: property object or name
  373. :type prop: qubes.property or str
  374. :rtype: qubes.property
  375. '''
  376. if isinstance(prop, qubes.property):
  377. return prop
  378. for p in cls.property_list():
  379. if p.__name__ == prop:
  380. return p
  381. raise AttributeError('No property {!r} found in {!r}'.format(
  382. prop, cls))
  383. def load_properties(self, load_stage=None):
  384. '''Load properties from immediate children of XML node.
  385. ``property-set`` events are not fired for each individual property.
  386. :param int load_stage: Stage of loading.
  387. '''
  388. if self.xml is None:
  389. return
  390. all_names = set(
  391. prop.__name__ for prop in self.property_list(load_stage))
  392. for node in self.xml.xpath('./properties/property'):
  393. name = node.get('name')
  394. value = node.get('ref') or node.text
  395. if not name in all_names:
  396. continue
  397. setattr(self, name, value)
  398. def xml_properties(self, with_defaults=False):
  399. '''Iterator that yields XML nodes representing set properties.
  400. :param bool with_defaults: If :py:obj:`True`, then it also includes \
  401. properties which were not set explicite, but have default values \
  402. filled.
  403. '''
  404. properties = lxml.etree.Element('properties')
  405. for prop in self.property_list():
  406. # pylint: disable=protected-access
  407. try:
  408. value = getattr(
  409. self, (prop.__name__ if with_defaults else prop._attr_name))
  410. except AttributeError:
  411. continue
  412. try:
  413. value = prop._saver(self, prop, value)
  414. except property.DontSave:
  415. continue
  416. element = lxml.etree.Element('property', name=prop.__name__)
  417. if prop.save_via_ref:
  418. element.set('ref', value)
  419. else:
  420. element.text = value
  421. properties.append(element)
  422. return properties
  423. # this was clone_attrs
  424. def clone_properties(self, src, proplist=None):
  425. '''Clone properties from other object.
  426. :param PropertyHolder src: source object
  427. :param list proplist: list of properties \
  428. (:py:obj:`None` or omit for all properties except those with \
  429. :py:attr:`property.clone` set to :py:obj:`False`)
  430. '''
  431. if proplist is None:
  432. proplist = [prop for prop in self.property_list()
  433. if prop.clone]
  434. else:
  435. proplist = [prop for prop in self.property_list()
  436. if prop.__name__ in proplist or prop in proplist]
  437. for prop in proplist:
  438. try:
  439. # pylint: disable=protected-access
  440. self._property_init(prop, getattr(src, prop._attr_name))
  441. except AttributeError:
  442. continue
  443. self.fire_event('clone-properties', src, proplist)
  444. def property_require(self, prop, allow_none=False, hard=False):
  445. '''Complain badly when property is not set.
  446. :param prop: property name or object
  447. :type prop: qubes.property or str
  448. :param bool allow_none: if :py:obj:`True`, don't complain if \
  449. :py:obj:`None` is found
  450. :param bool hard: if :py:obj:`True`, raise :py:class:`AssertionError`; \
  451. if :py:obj:`False`, log warning instead
  452. '''
  453. if isinstance(prop, qubes.property):
  454. prop = prop.__name__
  455. try:
  456. value = getattr(self, prop)
  457. if value is None and not allow_none:
  458. raise AttributeError()
  459. except AttributeError:
  460. # pylint: disable=no-member
  461. msg = 'Required property {!r} not set on {!r}'.format(prop, self)
  462. if hard:
  463. raise AssertionError(msg)
  464. else:
  465. # pylint: disable=no-member
  466. self.log.fatal(msg)
  467. # pylint: disable=wrong-import-position
  468. from qubes.vm import VMProperty
  469. from qubes.app import Qubes
  470. __all__ = [
  471. 'Label',
  472. 'PropertyHolder',
  473. 'Qubes',
  474. 'VMProperty',
  475. 'property',
  476. ]