From b7f0cf7d82eb925b6b5adbabcc6b1ed18c1f441b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 25 Jul 2017 01:00:04 +0200 Subject: [PATCH] vm: add API for watching changes in QubesDB Provide an API for use QubesDB.watch() inside of qubesd. Fixes QubesOS/qubes-issues#2940 --- qubes/vm/__init__.py | 52 ++++++++++++++++++++++++++++++++++++++++++++ qubes/vm/adminvm.py | 3 +++ qubes/vm/qubesvm.py | 17 +++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 7613c123..7b714038 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -24,6 +24,7 @@ '''Qubes Virtual Machines ''' +import asyncio import re import string @@ -265,6 +266,8 @@ class BaseVM(qubes.PropertyHolder): # self.app must be set before super().__init__, because some property # setters need working .app attribute #: mother :py:class:`qubes.Qubes` object + self._qdb_watch_paths = set() + self._qdb_connection_watch = None self.app = app super(BaseVM, self).__init__(xml, **kwargs) @@ -387,6 +390,55 @@ class BaseVM(qubes.PropertyHolder): ]).render(vm=self) return domain_config + def watch_qdb_path(self, path): + '''Add a QubesDB path to be watched. + + Each change to the path will cause `domain-qdb-change:path` event to be + fired. + You can call this method for example in response to + `domain-init` and `domain-load` events. + ''' + + if path not in self._qdb_watch_paths: + self._qdb_watch_paths.add(path) + if self._qdb_connection_watch: + self._qdb_connection_watch.watch(path) + + def _qdb_watch_reader(self, loop): + '''Callback when self._qdb_connection_watch.watch_fd() FD is + readable. + + Read reported event (watched path change) and fire appropriate event. + ''' + import qubesdb # pylint: disable=import-error + try: + path = self._qdb_connection_watch.read_watch() + for watched_path in self._qdb_watch_paths: + if watched_path == path or ( + watched_path.endswith('/') and + path.startswith(watched_path)): + self.fire_event('domain-qdb-change:' + watched_path, + path=path) + except qubesdb.DisconnectedError: + loop.remove_reader(self._qdb_connection_watch.watch_fd()) + self._qdb_connection_watch.close() + self._qdb_connection_watch = None + + def start_qdb_watch(self, name, loop=None): + '''Start watching QubesDB + + Calling this method in appropriate time is responsibility of child + class. + ''' + import qubesdb # pylint: disable=import-error + self._qdb_connection_watch = qubesdb.QubesDB(name) + if loop is None: + loop = asyncio.get_event_loop() + loop.add_reader(self._qdb_connection_watch.watch_fd(), + self._qdb_watch_reader, loop) + for path in self._qdb_watch_paths: + self._qdb_connection_watch.watch(path) + class VMProperty(qubes.property): '''Property that is referring to a VM diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index 7a336afe..2b5cd916 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -55,6 +55,9 @@ class AdminVM(qubes.vm.BaseVM): self._qdb_connection = None self._libvirt_domain = None + if not self.app.vmm.offline_mode: + self.start_qdb_watch('dom0') + def __str__(self): return self.name diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index ab4ef7fc..6bf4267c 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -248,6 +248,18 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): This event is a good place to add your custom entries to the qdb. + .. event:: domain-qdb-change:watched-path (subject, event, path) + + Fired when watched QubesDB entry is changed. See + :py:meth:`watch_qdb_path`. *watched-path* part of event name is + what path was registered for watching, *path* in event argument + is what actually have changed (which may be different if watching a + directory, i.e. a path with `/` at the end). + + :param subject: Event emitter (the qube object) + :param event: Event name (``'domain-qdb-change'``) + :param path: changed QubesDB path + .. event:: backup-get-files (subject, event) Collects additional file to be included in a backup. @@ -726,6 +738,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): if self.storage is None: self.storage = qubes.storage.Storage(self) + if not self.app.vmm.offline_mode and self.is_running(): + self.start_qdb_watch(self.name) + @qubes.events.handler('property-set:label') def on_property_set_label(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument @@ -1770,6 +1785,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.fire_event('domain-qdb-create') + self.start_qdb_watch(self.name) + # TODO async; update this in constructor def _update_libvirt_domain(self): '''Re-initialise :py:attr:`libvirt_domain`.'''