diff --git a/doc/conf.py b/doc/conf.py index f000ffcf..fb76e49e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -28,7 +28,17 @@ sys.path.insert(0, os.path.abspath('../')) # Add any Sphinx extension module names here, as strings. They can be extensions # 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. templates_path = ['_templates'] diff --git a/qubes/__init__.py b/qubes/__init__.py index 524223b0..792c8549 100644 --- a/qubes/__init__.py +++ b/qubes/__init__.py @@ -330,6 +330,9 @@ class VMCollection(object): if not isinstance(value, qubes.vm.BaseVM): 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: raise ValueError('This collection already holds VM that has qid={!r} (!r)'.format( value.qid, self[value.qid])) @@ -338,6 +341,7 @@ class VMCollection(object): value.name, self[value.name])) self._dict[value.qid] = value + self.app.fire_event('domain-added', value) def __getitem__(self, key): @@ -359,7 +363,9 @@ class VMCollection(object): 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): @@ -467,12 +473,28 @@ class property(object): 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: value = self._setter(instance, self, value) if self._type is not None: value = self._type(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): return '<{} object at {:#x} name={!r} default={!r}>'.format( @@ -505,8 +527,27 @@ class property(object): prop.__name__, self.__class__.__name__)) -class PropertyHolder(object): - '''Abstract class for holding :py:class:`qubes.property`''' +class PropertyHolder(qubes.events.Emitter): + '''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: (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): super(PropertyHolder, self).__init__(*args, **kwargs) @@ -545,11 +586,14 @@ class PropertyHolder(object): def load_properties(self, load_stage=None): '''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 ''' # 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)) # sys.stderr.write(' all_names={!r}\n'.format(all_names)) for node in self.xml.xpath('./properties/property'): @@ -563,6 +607,9 @@ class PropertyHolder(object): name, self.__class__)) setattr(self, name, value) + + self.events_enabled = True + self.fire_event('property-loaded') # sys.stderr.write(' load_properties return\n') @@ -630,22 +677,45 @@ class Qubes(PropertyHolder): :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 - (currently labels). + 1. In the first stage there are loaded some basic features from store + (currently labels). - In the second stage stubs for all VMs are loaded. They are filled with - their basic properties, like ``qid`` and ``name``. + 2. In the second stage stubs for all VMs are loaded. They are filled + with their basic properties, like ``qid`` and ``name``. - In the third stage all global properties are loaded. They often reference - VMs, like default netvm, so they should be filled after loading VMs. + 3. In the third stage all global properties are loaded. They often + 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 - all VMs loaded, because they represent dependencies between VMs like - aforementioned netvm. + 4. In the fourth stage all remaining VM properties are loaded. They + also need all VMs loaded, because they represent dependencies + 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, @@ -822,22 +892,16 @@ class Qubes(PropertyHolder): return labels + def add_new_vm(self, vm): '''Add new Virtual Machine to colletion ''' - if not hasattr(vm, 'qid'): - vm.qid = self.domains.get_new_unused_qid() - self.domains.add(vm) - # - # XXX - # all this will be moved to an event handler - # and deduplicated with self.load() - # - + @qubes.events.handler('domain-added') + def on_domain_addedd(self, event, vm): # make first created NetVM the default one if not hasattr(self, 'default_fw_netvm') \ and vm.provides_network \ @@ -866,23 +930,21 @@ class Qubes(PropertyHolder): and hasattr(vm, 'netvm'): 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 -# def pop(self, qid): -# if self.default_netvm_qid == qid: -# self.default_netvm_qid = None -# if self.default_fw_netvm_qid == qid: -# self.default_fw_netvm_qid = None -# if self.clockvm_qid == qid: -# self.clockvm_qid = None -# if self.updatevm_qid == qid: -# self.updatevm_qid = None -# if self.default_template_qid == qid: -# self.default_template_qid = None -# -# return super(QubesVmCollection, self).pop(qid) + @qubes.events.handler('domain-deleted') + def on_domain_deleted(self, event, vm): + if self.default_netvm == vm: + del self.default_netvm + if self.default_fw_netvm == vm: + del self.default_fw_netvm + if self.clockvm == vm: + del self.clockvm + if self.updatevm == vm: + del self.updatevm + if self.default_template == vm: + del self.default_template + + return super(QubesVmCollection, self).pop(qid) # load plugins diff --git a/qubes/dochelpers.py b/qubes/dochelpers.py index 5d0700bd..4c5a568c 100644 --- a/qubes/dochelpers.py +++ b/qubes/dochelpers.py @@ -10,6 +10,7 @@ particulary our custom Sphinx extension. import csv import posixpath +import re import sys import urllib2 @@ -18,7 +19,9 @@ import docutils.nodes import docutils.parsers.rst import docutils.parsers.rst.roles import docutils.statemachine +import sphinx import sphinx.locale +import sphinx.util.docfields def fetch_ticket_info(uri): '''Fetch info about particular trac ticket given @@ -116,6 +119,30 @@ class VersionCheck(docutils.parsers.rst.Directive): 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): app.add_role('ticket', ticket) app.add_config_value('ticket_base_uri', 'https://wiki.qubes-os.org/ticket/', 'env') @@ -124,5 +151,10 @@ def setup(app): man=(visit, depart)) 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 diff --git a/qubes/events.py b/qubes/events.py index c8cad69b..846b6626 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -57,6 +57,8 @@ class Emitter(object): def __init__(self, *args, **kwargs): super(Emitter, self).__init__(*args, **kwargs) + self.events_enabled = True + try: propnames = set(prop.__name__ for prop in self.get_props_list()) except AttributeError: @@ -95,6 +97,9 @@ class Emitter(object): different events. ''' + if not self.events_enabled: + return + for handler in self.__handlers__[event]: if hasattr(handler, 'ha_bound'): # this is our (bound) method, self is implicit diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 109197f9..e6ebbb3a 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -68,7 +68,7 @@ class BaseVMMeta(qubes.plugins.Plugin, qubes.events.EmitterMeta): cls.__hooks__ = collections.defaultdict(list) -class BaseVM(qubes.PropertyHolder, qubes.events.Emitter): +class BaseVM(qubes.PropertyHolder): '''Base class for all VMs :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.tags = tags + self.events_enabled = False all_names = set(prop.__name__ for prop in self.get_props_list(load_stage=2)) for key in list(kwargs.keys()): if not key in all_names: @@ -101,6 +102,10 @@ class BaseVM(qubes.PropertyHolder, qubes.events.Emitter): super(BaseVM, self).__init__(xml, *args, **kwargs) + self.events_enabled = True + self.fire_event('property-load') + + def add_new_vm(self, vm): '''Add new Virtual Machine to colletion