Browse Source

vm: add API for watching changes in QubesDB

Provide an API for use QubesDB.watch() inside of qubesd.

Fixes QubesOS/qubes-issues#2940
Marek Marczykowski-Górecki 6 years ago
parent
commit
b7f0cf7d82
3 changed files with 72 additions and 0 deletions
  1. 52 0
      qubes/vm/__init__.py
  2. 3 0
      qubes/vm/adminvm.py
  3. 17 0
      qubes/vm/qubesvm.py

+ 52 - 0
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

+ 3 - 0
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
 

+ 17 - 0
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`.'''