diff --git a/qubes/app.py b/qubes/app.py index b5e650e2b..a06052089 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -68,6 +68,7 @@ import qubes.vm.templatevm # pylint: enable=wrong-import-position + class VirDomainWrapper: # pylint: disable=too-few-public-methods @@ -149,17 +150,17 @@ def wrapper(*args, **kwargs): class VMMConnection: - '''Connection to Virtual Machine Manager (libvirt)''' + """Connection to Virtual Machine Manager (libvirt)""" def __init__(self, offline_mode=None, libvirt_reconnect_cb=None): - ''' + """ :param offline_mode: enable/disable offline mode; default is to enable when running in chroot as root, otherwise disable :param libvirt_reconnect_cb: callable to be called when connection to libvirt is re-established; the callback is called with old connection as argument - ''' + """ if offline_mode is None: offline_mode = bool(os.getuid() == 0 and os.stat('/') != os.stat('/proc/1/root/.')) @@ -172,16 +173,16 @@ def __init__(self, offline_mode=None, libvirt_reconnect_cb=None): @property def offline_mode(self): - '''Check or enable offline mode (do not actually connect to vmm)''' + """Check or enable offline mode (do not actually connect to vmm)""" return self._offline_mode def _libvirt_error_handler(self, ctx, error): pass def init_vmm_connection(self): - '''Initialise connection + """Initialise connection - This method is automatically called when getting''' + This method is automatically called when getting""" if self._libvirt_conn is not None: # Already initialized return @@ -201,16 +202,16 @@ def init_vmm_connection(self): @property def libvirt_conn(self): - '''Connection to libvirt''' + """Connection to libvirt""" self.init_vmm_connection() return self._libvirt_conn @property def xs(self): - '''Connection to Xen Store + """Connection to Xen Store This property in available only when running on Xen. - ''' + """ # XXX what about the case when we run under KVM, # but xen modules are importable? @@ -223,10 +224,10 @@ def xs(self): @property def xc(self): - '''Connection to Xen + """Connection to Xen This property in available only when running on Xen. - ''' + """ # XXX what about the case when we run under KVM, # but xen modules are importable? @@ -249,11 +250,11 @@ def close(self): class QubesHost: - '''Basic information about host machine + """Basic information about host machine :param qubes.Qubes app: Qubes application context (must have \ :py:attr:`Qubes.vmm` attribute defined) - ''' + """ def __init__(self, app): self.app = app @@ -261,7 +262,6 @@ def __init__(self, app): self._total_mem = None self._physinfo = None - def _fetch(self): if self._no_cpus is not None: return @@ -280,20 +280,18 @@ def _fetch(self): except NotImplementedError: pass - @property def memory_total(self): - '''Total memory, in kbytes''' + """Total memory, in kbytes""" if self.app.vmm.offline_mode: return 2**64-1 self._fetch() return self._total_mem - @property def no_cpus(self): - '''Number of CPUs''' + """Number of CPUs""" if self.app.vmm.offline_mode: return 42 @@ -301,21 +299,19 @@ def no_cpus(self): self._fetch() return self._no_cpus - def get_free_xen_memory(self): - '''Get free memory from Xen's physinfo. + """Get free memory from Xen's physinfo. :raises NotImplementedError: when not under Xen - ''' + """ try: self._physinfo = self.app.xc.physinfo() except AttributeError: raise NotImplementedError('This function requires Xen hypervisor') return int(self._physinfo['free_memory']) - def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): - '''Measure cpu usage for all domains at once. + """Measure cpu usage for all domains at once. If previous measurements are given, CPU usage will be given in percents of time. Otherwise only absolute value (seconds). @@ -339,7 +335,7 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): :param only_vm: get measurements only for this VM :raises NotImplementedError: when not under Xen - ''' + """ if (previous_time is None) != (previous is None): raise ValueError( @@ -386,61 +382,55 @@ def get_vm_stats(self, previous_time=None, previous=None, only_vm=None): class VMCollection: - '''A collection of Qubes VMs + """A collection of Qubes VMs VMCollection supports ``in`` operator. You may test for ``qid``, ``name`` and whole VM object's presence. Iterating over VMCollection will yield machine objects. - ''' + """ def __init__(self, app): self.app = app self._dict = dict() - def close(self): del self.app self._dict.clear() del self._dict - def __repr__(self): return '<{} {!r}>'.format( self.__class__.__name__, list(sorted(self.keys()))) - def items(self): - '''Iterate over ``(qid, vm)`` pairs''' + """Iterate over ``(qid, vm)`` pairs""" for qid in self.qids(): yield (qid, self[qid]) - def qids(self): - '''Iterate over all qids + """Iterate over all qids qids are sorted by numerical order. - ''' + """ return iter(sorted(self._dict.keys())) keys = qids - def names(self): - '''Iterate over all names + """Iterate over all names names are sorted by lexical order. - ''' + """ return iter(sorted(vm.name for vm in self._dict.values())) - def vms(self): - '''Iterate over all machines + """Iterate over all machines vms are sorted by qid. - ''' + """ return iter(sorted(self._dict.values())) @@ -448,12 +438,12 @@ def vms(self): values = vms def add(self, value, _enable_events=True): - '''Add VM to collection + """Add VM to collection :param qubes.vm.BaseVM value: VM to add :raises TypeError: when value is of wrong type :raises ValueError: when there is already VM which has equal ``qid`` - ''' + """ # this violates duck typing, but is needed # for VMProperty to function correctly @@ -518,17 +508,14 @@ def __contains__(self, key): return any((key in (vm, vm.qid, vm.name)) for vm in self) - def __len__(self): return len(self._dict) - def get_vms_based_on(self, template): template = self[template] return set(vm for vm in self if hasattr(vm, 'template') and vm.template == template) - def get_vms_connected_to(self, netvm): new_vms = set([self[netvm]]) dependent_vms = set() @@ -548,7 +535,6 @@ def get_vms_connected_to(self, netvm): return dependent_vms - # XXX with Qubes Admin Api this will probably lead to race condition # whole process of creating and adding should be synchronised def get_new_unused_qid(self): @@ -558,7 +544,6 @@ def get_new_unused_qid(self): return i raise LookupError("Cannot find unused qid!") - def get_new_unused_dispid(self): for _ in range(int(qubes.config.max_dispid ** 0.5)): dispid = random.SystemRandom().randrange(qubes.config.max_dispid) @@ -570,13 +555,13 @@ def get_new_unused_dispid(self): def _default_pool(app): - ''' Default storage pool. + """ Default storage pool. 1. If there is one named 'default', use it. 2. Check if root fs is on LVM thin - use that 3. Look for file(-reflink)-based pool pointing to /var/lib/qubes 4. Fail - ''' + """ if 'default' in app.pools: return app.pools['default'] @@ -606,6 +591,7 @@ def _default_pool(app): return pool raise AttributeError('Cannot determine default storage pool') + def _setter_pool(app, prop, value): if isinstance(value, qubes.storage.Pool): return value @@ -615,6 +601,7 @@ def _setter_pool(app, prop, value): raise qubes.exc.QubesPropertyValueError(app, prop, value, 'No such storage pool') + def _setter_default_netvm(app, prop, value): # skip netvm loop check while loading qubes.xml, to avoid tricky loading # order @@ -637,7 +624,7 @@ def _setter_default_netvm(app, prop, value): class Qubes(qubes.PropertyHolder): - '''Main Qubes application + """Main Qubes application :param str store: path to ``qubes.xml`` @@ -722,21 +709,23 @@ class Qubes(qubes.PropertyHolder): :param pool: Pool object Methods and attributes: - ''' - + """ + default_guivm = qubes.VMProperty('default_guivm', load_stage=3, + default=None, allow_none=True, + doc='Default GuiVM for VMs.') default_netvm = qubes.VMProperty('default_netvm', load_stage=3, default=None, allow_none=True, setter=_setter_default_netvm, - doc='''Default NetVM for AppVMs. Initial state is `None`, which means - that AppVMs are not connected to the Internet.''') + doc="""Default NetVM for AppVMs. Initial state is `None`, which means + that AppVMs are not connected to the Internet.""") default_template = qubes.VMProperty('default_template', load_stage=3, vmclass=qubes.vm.templatevm.TemplateVM, doc='Default template for new AppVMs', allow_none=True) updatevm = qubes.VMProperty('updatevm', load_stage=3, default=None, allow_none=True, - doc='''Which VM to use as `yum` proxy for updating AdminVM and - TemplateVMs''') + doc="""Which VM to use as `yum` proxy for updating AdminVM and + TemplateVMs""") clockvm = qubes.VMProperty('clockvm', load_stage=3, default=None, allow_none=True, doc='Which VM to use as NTP proxy for updating AdminVM') @@ -780,14 +769,14 @@ class Qubes(qubes.PropertyHolder): load_stage=3, default=60, type=int, - doc='''Default time in seconds after which qrexec connection attempt is - deemed failed''') + doc="""Default time in seconds after which qrexec connection attempt is + deemed failed""") default_shutdown_timeout = qubes.property('default_shutdown_timeout', load_stage=3, default=60, type=int, - doc='''Default time in seconds for VM shutdown to complete''') + doc="""Default time in seconds for VM shutdown to complete""") stats_interval = qubes.property('stats_interval', load_stage=3, @@ -861,7 +850,7 @@ def store(self): return self._store def _migrate_global_properties(self): - '''Migrate renamed/dropped properties''' + """Migrate renamed/dropped properties""" if self.xml is None: return @@ -905,12 +894,12 @@ def _migrate_global_properties(self): node_default_fw_netvm.getparent().remove(node_default_fw_netvm) def load(self, lock=False): - '''Open qubes.xml + """Open qubes.xml :throws EnvironmentError: failure on parsing store :throws xml.parsers.expat.ExpatError: failure on parsing store :raises lxml.etree.XMLSyntaxError: on syntax error in qubes.xml - ''' + """ fh = self._acquire_lock() self.xml = lxml.etree.parse(fh) @@ -954,6 +943,7 @@ def load(self, lock=False): # stage 5: misc fixups + self.property_require('default_guivm', allow_none=True) self.property_require('default_netvm', allow_none=True) self.property_require('default_template', allow_none=True) self.property_require('clockvm', allow_none=True) @@ -970,7 +960,6 @@ def load(self, lock=False): if not lock: self._release_lock() - def __xml__(self): element = lxml.etree.Element('qubes') @@ -997,7 +986,7 @@ def __str__(self): return type(self).__name__ def save(self, lock=True): - '''Save all data to qubes.xml + """Save all data to qubes.xml There are several problems with saving :file:`qubes.xml` which must be mitigated: @@ -1009,7 +998,7 @@ def save(self, lock=True): :param bool lock: keep file locked after saving :throws EnvironmentError: failure on saving - ''' + """ if not self.__locked_fh: self._acquire_lock(for_save=True) @@ -1039,11 +1028,10 @@ def save(self, lock=True): if not lock: self._release_lock() - def close(self): - '''Deconstruct the object and break circular references + """Deconstruct the object and break circular references - After calling this the object is unusable, not even for saving.''' + After calling this the object is unusable, not even for saving.""" self.log.debug('close() <- %#x', id(self)) for frame in traceback.extract_stack(): @@ -1073,7 +1061,6 @@ def close(self): if self.__locked_fh: self._release_lock() - def _acquire_lock(self, for_save=False): assert self.__locked_fh is None, 'double lock' @@ -1116,7 +1103,6 @@ def _acquire_lock(self, for_save=False): self.__locked_fh = os.fdopen(fd, 'r+b') return self.__locked_fh - def _release_lock(self): assert self.__locked_fh is not None, 'double release' @@ -1125,7 +1111,6 @@ def _release_lock(self): self.__locked_fh.close() self.__locked_fh = None - def load_initial_values(self): self.labels = { 1: qubes.Label(1, '0xcc0000', 'red'), @@ -1181,12 +1166,11 @@ def create_empty_store(cls, *args, **kwargs): return self - def xml_labels(self): - '''Serialise labels + """Serialise labels :rtype: lxml.etree._Element - ''' + """ labels = lxml.etree.Element('labels') for label in sorted(self.labels.values(), key=lambda labl: labl.index): @@ -1195,14 +1179,14 @@ def xml_labels(self): @staticmethod def get_vm_class(clsname): - '''Find the class for a domain. + """Find the class for a domain. Classes are registered as setuptools' entry points in ``qubes.vm`` group. Any package may supply their own classes. :param str clsname: name of the class :return type: class - ''' + """ try: return qubes.utils.get_entry_point_one( @@ -1213,9 +1197,9 @@ def get_vm_class(clsname): # don't catch TypeError def add_new_vm(self, cls, qid=None, **kwargs): - '''Add new Virtual Machine to collection + """Add new Virtual Machine to collection - ''' + """ if qid is None: qid = self.domains.get_new_unused_qid() @@ -1239,10 +1223,10 @@ def add_new_vm(self, cls, qid=None, **kwargs): return self.domains.add(cls(self, None, qid=qid, **kwargs)) def get_label(self, label): - '''Get label as identified by index or name + """Get label as identified by index or name :throws KeyError: when label is not found - ''' + """ # first search for index, verbatim try: @@ -1308,9 +1292,8 @@ def remove_pool(self, name): except KeyError: return - def get_pool(self, pool): - ''' Returns a :py:class:`qubes.storage.Pool` instance ''' + """ Returns a :py:class:`qubes.storage.Pool` instance """ if isinstance(pool, qubes.storage.Pool): return pool try: @@ -1341,10 +1324,10 @@ def _get_pool(**kwargs): (driver, name)) def register_event_handlers(self, old_connection=None): - '''Register libvirt event handlers, which will translate libvirt + """Register libvirt event handlers, which will translate libvirt events into qubes.events. This function should be called only in 'qubesd' process and only when mainloop has been already set. - ''' + """ if old_connection: try: old_connection.domainEventDeregisterAny( @@ -1361,9 +1344,9 @@ def register_event_handlers(self, old_connection=None): None)) def _domain_event_callback(self, _conn, domain, event, _detail, _opaque): - '''Generic libvirt event handler (virConnectDomainEventCallback), + """Generic libvirt event handler (virConnectDomainEventCallback), translate libvirt event into qubes.events. - ''' + """ if not self.events_enabled: return @@ -1414,6 +1397,7 @@ def on_domain_pre_deleted(self, event, vm): def on_domain_deleted(self, event, vm): # pylint: disable=unused-argument for propname in ( + 'default_guivm' 'default_netvm', 'default_fw_netvm', 'clockvm', @@ -1426,7 +1410,6 @@ def on_domain_deleted(self, event, vm): except AttributeError: pass - @qubes.events.handler('property-pre-set:clockvm') def on_property_pre_set_clockvm(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument,no-self-use @@ -1452,7 +1435,6 @@ def on_property_pre_set_default_netvm(self, event, name, newvalue, 'Cannot change {!r} to domain that ' 'is not running ({!r}).'.format(name, newvalue.name)) - @qubes.events.handler('property-set:default_fw_netvm') def on_property_set_default_fw_netvm(self, event, name, newvalue, oldvalue=None): @@ -1467,7 +1449,6 @@ def on_property_set_default_fw_netvm(self, event, name, newvalue, vm.fire_event('property-del:netvm', name='netvm', oldvalue=oldvalue) - @qubes.events.handler('property-set:default_netvm') def on_property_set_default_netvm(self, event, name, newvalue, oldvalue=None): diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index b495c100e..e026ea283 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -29,17 +29,38 @@ class GUI(qubes.ext.Extension): # TODO put this somewhere... @staticmethod def send_gui_mode(vm): - vm.run_service('qubes.SetGuiMode', - input=('SEAMLESS' - if vm.features.get('gui-seamless', False) + vm.run_service('qubes.SetGuiMode', input=( + 'SEAMLESS' if vm.features.get('gui-seamless', False) else 'FULLSCREEN')) + @qubes.ext.handler('property-set:guivm') + def on_property_set(self, subject, event, name, newvalue, oldvalue=None): + # Clean other 'guivm-XXX' tags. + # gui-daemon can connect to only one domain + tags_list = list(subject.tags) + for tag in tags_list: + if 'guivm-' in tag: + subject.tags.remove(tag) + + guivm = 'guivm-' + newvalue.name + subject.tags.add(guivm) + @qubes.ext.handler('domain-qdb-create') def on_domain_qdb_create(self, vm, event): # pylint: disable=unused-argument,no-self-use for feature in ('gui-videoram-overhead', 'gui-videoram-min'): try: - vm.untrusted_qdb.write('/qubes-{}'.format(feature), + vm.untrusted_qdb.write( + '/qubes-{}'.format(feature), vm.features.check_with_template_and_adminvm(feature)) except KeyError: pass + + # Add GuiVM Xen ID for gui-daemon + try: + if vm.guivm is not None: + if str(vm.name) != str(vm.guivm.name): + vm.untrusted_qdb.write('/qubes-gui-domain-xid', + str(vm.guivm.xid)) + except AttributeError: + vm.untrusted_qdb.write('/qubes-gui-domain-xid', '') diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index a3e3e4a7c..6e0be4d08 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -509,6 +509,10 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): # properties loaded from XML # + guivm = qubes.VMProperty('guivm', load_stage=4, allow_none=True, + default=(lambda self: self.app.default_guivm), + doc='VM used for Gui') + virt_mode = qubes.property('virt_mode', type=str, setter=_setter_virt_mode, default=_default_virt_mode, @@ -618,6 +622,12 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): type=int, doc='Time of last backup of the qube, in seconds since unix epoch') + default_guivm = qubes.VMProperty('default_guivm', + load_stage=4, + allow_none=True, + default=(lambda self: self.app.default_guivm), + doc='Default GuiVM for VMs.') + default_dispvm = qubes.VMProperty('default_dispvm', load_stage=4, allow_none=True, @@ -2112,15 +2122,13 @@ def create_qdb_entries(self): self.untrusted_qdb.write('/qubes-ip6', str(self.visible_ip6)) if self.visible_gateway6: # pylint: disable=using-constant-test self.untrusted_qdb.write('/qubes-gateway6', - str(self.visible_gateway6)) - + str(self.visible_gateway6)) tzname = qubes.utils.get_timezone() if tzname: self.untrusted_qdb.write('/qubes-timezone', tzname) self.untrusted_qdb.write('/qubes-block-devices', '') - self.untrusted_qdb.write('/qubes-usb-devices', '') # TODO: Currently the whole qmemman is quite Xen-specific, so stay with