core3: basic global events and their documentation

This commit is contained in:
Wojtek Porczyk 2014-12-09 18:34:00 +01:00
parent 855a434879
commit 1a032ecf2a
5 changed files with 155 additions and 41 deletions

View File

@ -28,7 +28,17 @@ sys.path.insert(0, os.path.abspath('../'))
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'qubes.dochelpers'] extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
'sphinx.ext.coverage',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
'qubes.dochelpers',
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']

View File

@ -330,6 +330,9 @@ class VMCollection(object):
if not isinstance(value, qubes.vm.BaseVM): if not isinstance(value, qubes.vm.BaseVM):
raise TypeError('{} holds only BaseVM instances'.format(self.__class__.__name__)) raise TypeError('{} holds only BaseVM instances'.format(self.__class__.__name__))
if not hasattr(value, 'qid'):
value.qid = self.domains.get_new_unused_qid()
if value.qid in self: if value.qid in self:
raise ValueError('This collection already holds VM that has qid={!r} (!r)'.format( raise ValueError('This collection already holds VM that has qid={!r} (!r)'.format(
value.qid, self[value.qid])) value.qid, self[value.qid]))
@ -338,6 +341,7 @@ class VMCollection(object):
value.name, self[value.name])) value.name, self[value.name]))
self._dict[value.qid] = value self._dict[value.qid] = value
self.app.fire_event('domain-added', value)
def __getitem__(self, key): def __getitem__(self, key):
@ -359,7 +363,9 @@ class VMCollection(object):
def __delitem__(self, key): def __delitem__(self, key):
del self._dict[self[key].qid] vm = self[key]
del self._dict[vm.qid]
self.app.fire_event('domain-deleted', vm)
def __contains__(self, key): def __contains__(self, key):
@ -467,12 +473,28 @@ class property(object):
def __set__(self, instance, value): def __set__(self, instance, value):
try:
oldvalue = getattr(instance, self.__name__)
has_oldvalue = True
except AttributeError:
has_oldvalue = False
if self._setter is not None: if self._setter is not None:
value = self._setter(instance, self, value) value = self._setter(instance, self, value)
if self._type is not None: if self._type is not None:
value = self._type(value) value = self._type(value)
instance._init_property(self, value) instance._init_property(self, value)
if has_oldvalue:
instance.fire_event('property-set:' + self.__name__, value, oldvalue)
else:
instance.fire_event('property-set:' + self.__name__, value)
def __delete__(self, instance):
delattr(instance, self._attr_name)
def __repr__(self): def __repr__(self):
return '<{} object at {:#x} name={!r} default={!r}>'.format( return '<{} object at {:#x} name={!r} default={!r}>'.format(
@ -505,8 +527,27 @@ class property(object):
prop.__name__, self.__class__.__name__)) prop.__name__, self.__class__.__name__))
class PropertyHolder(object): class PropertyHolder(qubes.events.Emitter):
'''Abstract class for holding :py:class:`qubes.property`''' '''Abstract class for holding :py:class:`qubes.property`
Events fired by instances of this class:
.. event:: property-load (subject, event)
Fired once after all properties are loaded from XML. Individual
``property-set`` events are not fired.
.. event:: property-set:<propname> (subject, event, name, newvalue[, oldvalue])
Fired when property changes state. Signature is variable, *oldvalue* is
present only if there was an old value.
:param name: Property name
:param newvalue: New value of the property
:param oldvalue: Old value of the property
Members:
'''
def __init__(self, xml, *args, **kwargs): def __init__(self, xml, *args, **kwargs):
super(PropertyHolder, self).__init__(*args, **kwargs) super(PropertyHolder, self).__init__(*args, **kwargs)
@ -545,11 +586,14 @@ class PropertyHolder(object):
def load_properties(self, load_stage=None): def load_properties(self, load_stage=None):
'''Load properties from immediate children of XML node. '''Load properties from immediate children of XML node.
``property-set`` events are not fired for each individual property.
:param lxml.etree._Element xml: XML node reference :param lxml.etree._Element xml: XML node reference
''' '''
# sys.stderr.write('<{}>.load_properties(load_stage={}) xml={!r}\n'.format(hex(id(self)), load_stage, self.xml)) # sys.stderr.write('<{}>.load_properties(load_stage={}) xml={!r}\n'.format(hex(id(self)), load_stage, self.xml))
self.events_enabled = False
all_names = set(prop.__name__ for prop in self.get_props_list(load_stage)) all_names = set(prop.__name__ for prop in self.get_props_list(load_stage))
# sys.stderr.write(' all_names={!r}\n'.format(all_names)) # sys.stderr.write(' all_names={!r}\n'.format(all_names))
for node in self.xml.xpath('./properties/property'): for node in self.xml.xpath('./properties/property'):
@ -563,6 +607,9 @@ class PropertyHolder(object):
name, self.__class__)) name, self.__class__))
setattr(self, name, value) setattr(self, name, value)
self.events_enabled = True
self.fire_event('property-loaded')
# sys.stderr.write(' load_properties return\n') # sys.stderr.write(' load_properties return\n')
@ -630,22 +677,45 @@ class Qubes(PropertyHolder):
:param str store: path to ``qubes.xml`` :param str store: path to ``qubes.xml``
The store is loaded in stages. The store is loaded in stages:
In the first stage there are loaded some basic features from store 1. In the first stage there are loaded some basic features from store
(currently labels). (currently labels).
In the second stage stubs for all VMs are loaded. They are filled with 2. In the second stage stubs for all VMs are loaded. They are filled
their basic properties, like ``qid`` and ``name``. with their basic properties, like ``qid`` and ``name``.
In the third stage all global properties are loaded. They often reference 3. In the third stage all global properties are loaded. They often
VMs, like default netvm, so they should be filled after loading VMs. reference VMs, like default netvm, so they should be filled after
loading VMs.
In the fourth stage all remaining VM properties are loaded. They also need 4. In the fourth stage all remaining VM properties are loaded. They
all VMs loaded, because they represent dependencies between VMs like also need all VMs loaded, because they represent dependencies
aforementioned netvm. between VMs like aforementioned netvm.
In the fifth stage there are some fixups to ensure sane system operation. 5. In the fifth stage there are some fixups to ensure sane system
operation.
This class emits following events:
.. event:: domain-added (subject, event, vm)
When domain is added.
:param subject: Event emitter
:param event: Event name (``'domain-added'``)
:param vm: Domain object
.. event:: domain-deleted (subject, event, vm)
When domain is deleted. VM still has reference to ``app`` object,
but is not contained within VMCollection.
:param subject: Event emitter
:param event: Event name (``'domain-deleted'``)
:param vm: Domain object
Methods and attributes:
''' '''
default_netvm = VMProperty('default_netvm', load_stage=3, default_netvm = VMProperty('default_netvm', load_stage=3,
@ -822,22 +892,16 @@ class Qubes(PropertyHolder):
return labels return labels
def add_new_vm(self, vm): def add_new_vm(self, vm):
'''Add new Virtual Machine to colletion '''Add new Virtual Machine to colletion
''' '''
if not hasattr(vm, 'qid'):
vm.qid = self.domains.get_new_unused_qid()
self.domains.add(vm) self.domains.add(vm)
# @qubes.events.handler('domain-added')
# XXX def on_domain_addedd(self, event, vm):
# all this will be moved to an event handler
# and deduplicated with self.load()
#
# make first created NetVM the default one # make first created NetVM the default one
if not hasattr(self, 'default_fw_netvm') \ if not hasattr(self, 'default_fw_netvm') \
and vm.provides_network \ and vm.provides_network \
@ -866,23 +930,21 @@ class Qubes(PropertyHolder):
and hasattr(vm, 'netvm'): and hasattr(vm, 'netvm'):
self.default_clockvm = vm self.default_clockvm = vm
# XXX don't know if it should return self
return vm
# XXX This was in QubesVmCollection, will be in an event @qubes.events.handler('domain-deleted')
# def pop(self, qid): def on_domain_deleted(self, event, vm):
# if self.default_netvm_qid == qid: if self.default_netvm == vm:
# self.default_netvm_qid = None del self.default_netvm
# if self.default_fw_netvm_qid == qid: if self.default_fw_netvm == vm:
# self.default_fw_netvm_qid = None del self.default_fw_netvm
# if self.clockvm_qid == qid: if self.clockvm == vm:
# self.clockvm_qid = None del self.clockvm
# if self.updatevm_qid == qid: if self.updatevm == vm:
# self.updatevm_qid = None del self.updatevm
# if self.default_template_qid == qid: if self.default_template == vm:
# self.default_template_qid = None del self.default_template
#
# return super(QubesVmCollection, self).pop(qid) return super(QubesVmCollection, self).pop(qid)
# load plugins # load plugins

View File

@ -10,6 +10,7 @@ particulary our custom Sphinx extension.
import csv import csv
import posixpath import posixpath
import re
import sys import sys
import urllib2 import urllib2
@ -18,7 +19,9 @@ import docutils.nodes
import docutils.parsers.rst import docutils.parsers.rst
import docutils.parsers.rst.roles import docutils.parsers.rst.roles
import docutils.statemachine import docutils.statemachine
import sphinx
import sphinx.locale import sphinx.locale
import sphinx.util.docfields
def fetch_ticket_info(uri): def fetch_ticket_info(uri):
'''Fetch info about particular trac ticket given '''Fetch info about particular trac ticket given
@ -116,6 +119,30 @@ class VersionCheck(docutils.parsers.rst.Directive):
return [node] return [node]
#
# this is lifted from sphinx' own conf.py
#
event_sig_re = re.compile(r'([a-zA-Z-]+)\s*\((.*)\)')
def parse_event(env, sig, signode):
m = event_sig_re.match(sig)
if not m:
signode += sphinx.addnodes.desc_name(sig, sig)
return sig
name, args = m.groups()
signode += sphinx.addnodes.desc_name(name, name)
plist = sphinx.addnodes.desc_parameterlist()
for arg in args.split(','):
arg = arg.strip()
plist += sphinx.addnodes.desc_parameter(arg, arg)
signode += plist
return name
#
# end of codelifting
#
def setup(app): def setup(app):
app.add_role('ticket', ticket) app.add_role('ticket', ticket)
app.add_config_value('ticket_base_uri', 'https://wiki.qubes-os.org/ticket/', 'env') app.add_config_value('ticket_base_uri', 'https://wiki.qubes-os.org/ticket/', 'env')
@ -124,5 +151,10 @@ def setup(app):
man=(visit, depart)) man=(visit, depart))
app.add_directive('versioncheck', VersionCheck) app.add_directive('versioncheck', VersionCheck)
fdesc = sphinx.util.docfields.GroupedField('parameter', label='Parameters',
names=['param'], can_collapse=True)
app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
doc_field_types=[fdesc])
# vim: ts=4 sw=4 et # vim: ts=4 sw=4 et

View File

@ -57,6 +57,8 @@ class Emitter(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Emitter, self).__init__(*args, **kwargs) super(Emitter, self).__init__(*args, **kwargs)
self.events_enabled = True
try: try:
propnames = set(prop.__name__ for prop in self.get_props_list()) propnames = set(prop.__name__ for prop in self.get_props_list())
except AttributeError: except AttributeError:
@ -95,6 +97,9 @@ class Emitter(object):
different events. different events.
''' '''
if not self.events_enabled:
return
for handler in self.__handlers__[event]: for handler in self.__handlers__[event]:
if hasattr(handler, 'ha_bound'): if hasattr(handler, 'ha_bound'):
# this is our (bound) method, self is implicit # this is our (bound) method, self is implicit

View File

@ -68,7 +68,7 @@ class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta):
cls.__hooks__ = collections.defaultdict(list) cls.__hooks__ = collections.defaultdict(list)
class BaseVM(qubes.PropertyHolder, qubes.events.Emitter): class BaseVM(qubes.PropertyHolder):
'''Base class for all VMs '''Base class for all VMs
:param app: Qubes application context :param app: Qubes application context
@ -90,6 +90,7 @@ class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
self.devices = collections.defaultdict(list) if devices is None else devices self.devices = collections.defaultdict(list) if devices is None else devices
self.tags = tags self.tags = tags
self.events_enabled = False
all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2)) all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2))
for key in list(kwargs.keys()): for key in list(kwargs.keys()):
if not key in all_names: if not key in all_names:
@ -101,6 +102,10 @@ class BaseVM(qubes.PropertyHolder, qubes.events.Emitter):
super(BaseVM, self).__init__(xml, *args, **kwargs) super(BaseVM, self).__init__(xml, *args, **kwargs)
self.events_enabled = True
self.fire_event('property-load')
def add_new_vm(self, vm): def add_new_vm(self, vm):
'''Add new Virtual Machine to colletion '''Add new Virtual Machine to colletion