__init__.py 20 KB

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