Browse Source

Merge branch 'qdb-watch'

* qdb-watch:
  tests: add qdb_watch test
  ext/block: make use of QubesDB watch
  vm: add API for watching changes in QubesDB
  vm: optimize imports
  api/admin: don't send internal events in admin.Events
  Add explanation why admin.vm.volume.Import is a custom script
  Follow change of qubesdb path return type
  Rename vm.qdb to vm.untrusted_qdb
Marek Marczykowski-Górecki 7 years ago
parent
commit
639fa26079

+ 23 - 0
qubes-rpc/admin.vm.volume.Import

@@ -1,4 +1,27 @@
 #!/bin/sh
 #!/bin/sh
+#
+# This Admin API call is implemented as a custom script, instead of dumb
+# passthrough to qubesd because it may get huge amount of data (whole root.img
+# for example). qubesd cannot handle it because:
+#  1. It loads the whole payload into memory, before even start looking at it
+#     (and later, do not allow to modify/append it).
+#  2. There is 64kB limit on payload size that qubesd can handle (because of
+#     point 1).
+#  3. Performance reasons (qubesd is not optimized for performance, passing
+#     such large data stream through it would take ages).
+#
+# The whole admin.vm.volume.Import consists of:
+#    1. Permissions checks, getting a path from appropriate storage pool (done
+#       by qubesd)
+#    2. Actual data import (done by this script, using dd)
+#    3. Report final result, produce final response to the caller (done by
+#       qubesd)
+#    
+#    This way we do not pass all the data through qubesd, but still can
+#    control the process from there in a meaningful way. Note that the last
+#    part (second call to qubesd) may perform all kind of verification (like
+#    a signature check on the data, or so) and can also prevent VM from
+#    starting (hooking also domain-pre-start event) from not verified image.
 
 
 set -e
 set -e
 
 

+ 10 - 0
qubes/api/admin.py

@@ -51,8 +51,18 @@ class QubesMgmtEventsDispatcher(object):
         self.send_event = send_event
         self.send_event = send_event
 
 
     def vm_handler(self, subject, event, **kwargs):
     def vm_handler(self, subject, event, **kwargs):
+        # do not send internal events
         if event.startswith('admin-permission:'):
         if event.startswith('admin-permission:'):
             return
             return
+        if event.startswith('device-get:'):
+            return
+        if event.startswith('device-list:'):
+            return
+        if event.startswith('device-list-attached:'):
+            return
+        if event in ('domain-is-fully-usable',):
+            return
+
         if not list(qubes.api.apply_filters([(subject, event, kwargs)],
         if not list(qubes.api.apply_filters([(subject, event, kwargs)],
                 self.filters)):
                 self.filters)):
             return
             return

+ 4 - 4
qubes/api/misc.py

@@ -51,10 +51,9 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI):
 
 
         prefix = '/features-request/'
         prefix = '/features-request/'
 
 
-        keys = [key.decode('ascii', errors='strict')
-            for key in self.src.qdb.list(prefix)]
+        keys = self.src.untrusted_qdb.list(prefix)
         untrusted_features = {key[len(prefix):]:
         untrusted_features = {key[len(prefix):]:
-            self.src.qdb.read(key).decode('ascii', errors='strict')
+            self.src.untrusted_qdb.read(key).decode('ascii', errors='strict')
                 for key in keys}
                 for key in keys}
 
 
         safe_set = string.ascii_letters + string.digits
         safe_set = string.ascii_letters + string.digits
@@ -79,7 +78,8 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI):
         safe_set = string.ascii_letters + string.digits
         safe_set = string.ascii_letters + string.digits
         expected_features = ('qrexec', 'gui', 'default-user')
         expected_features = ('qrexec', 'gui', 'default-user')
         for feature in expected_features:
         for feature in expected_features:
-            untrusted_value = self.src.qdb.read('/qubes-tools/' + feature)
+            untrusted_value = self.src.untrusted_qdb.read(
+                '/qubes-tools/' + feature)
             if untrusted_value:
             if untrusted_value:
                 untrusted_value = untrusted_value.decode('ascii',
                 untrusted_value = untrusted_value.decode('ascii',
                     errors='strict')
                     errors='strict')

+ 6 - 0
qubes/devices.py

@@ -44,11 +44,17 @@ Such extension should provide:
  domain of given identifier
  domain of given identifier
  - handle `device-list-attached:class` event - list currently attached
  - handle `device-list-attached:class` event - list currently attached
  devices to this domain
  devices to this domain
+ - fire `device-list-change:class` event when device list change is detected
+ (new/removed device)
 
 
 Note that device-listing event handlers can not be asynchronous. This for
 Note that device-listing event handlers can not be asynchronous. This for
 example means you can not call qrexec service there. This is intentional to
 example means you can not call qrexec service there. This is intentional to
 keep device listing operation cheap. You need to design the extension to take
 keep device listing operation cheap. You need to design the extension to take
 this into account (for example by using QubesDB).
 this into account (for example by using QubesDB).
+
+Extension may use QubesDB watch API (QubesVM.watch_qdb_path(path), then handle
+`domain-qdb-change:path`) to detect changes and fire
+`device-list-change:class` event.
 '''
 '''
 import asyncio
 import asyncio
 
 

+ 20 - 9
qubes/ext/block.py

@@ -56,7 +56,7 @@ class BlockDevice(qubes.devices.DeviceInfo):
                 return self.ident
                 return self.ident
             safe_set = {ord(c) for c in
             safe_set = {ord(c) for c in
                 string.ascii_letters + string.digits + '()+,-.:=_/ '}
                 string.ascii_letters + string.digits + '()+,-.:=_/ '}
-            untrusted_desc = self.backend_domain.qdb.read(
+            untrusted_desc = self.backend_domain.untrusted_qdb.read(
                 '/qubes-block-devices/{}/desc'.format(self.ident))
                 '/qubes-block-devices/{}/desc'.format(self.ident))
             desc = ''.join((chr(c) if c in safe_set else '_')
             desc = ''.join((chr(c) if c in safe_set else '_')
                 for c in untrusted_desc)
                 for c in untrusted_desc)
@@ -69,7 +69,7 @@ class BlockDevice(qubes.devices.DeviceInfo):
         if self._mode is None:
         if self._mode is None:
             if not self.backend_domain.is_running():
             if not self.backend_domain.is_running():
                 return 'w'
                 return 'w'
-            untrusted_mode = self.backend_domain.qdb.read(
+            untrusted_mode = self.backend_domain.untrusted_qdb.read(
                 '/qubes-block-devices/{}/mode'.format(self.ident))
                 '/qubes-block-devices/{}/mode'.format(self.ident))
             if untrusted_mode is None:
             if untrusted_mode is None:
                 self._mode = 'w'
                 self._mode = 'w'
@@ -87,7 +87,7 @@ class BlockDevice(qubes.devices.DeviceInfo):
         if self._size is None:
         if self._size is None:
             if not self.backend_domain.is_running():
             if not self.backend_domain.is_running():
                 return None
                 return None
-            untrusted_size = self.backend_domain.qdb.read(
+            untrusted_size = self.backend_domain.untrusted_qdb.read(
                 '/qubes-block-devices/{}/size'.format(self.ident))
                 '/qubes-block-devices/{}/size'.format(self.ident))
             if untrusted_size is None:
             if untrusted_size is None:
                 self._size = 0
                 self._size = 0
@@ -106,6 +106,18 @@ class BlockDevice(qubes.devices.DeviceInfo):
 
 
 
 
 class BlockDeviceExtension(qubes.ext.Extension):
 class BlockDeviceExtension(qubes.ext.Extension):
+    @qubes.ext.handler('domain-init', 'domain-load')
+    def on_domain_init_load(self, vm, event):
+        '''Initialize watching for changes'''
+        # pylint: disable=unused-argument,no-self-use
+        vm.watch_qdb_path('/qubes-block-devices')
+
+    @qubes.ext.handler('domain-qdb-change:/qubes-block-devices')
+    def on_qdb_change(self, vm, event, path):
+        '''A change in QubesDB means a change in device list'''
+        # pylint: disable=unused-argument,no-self-use
+        vm.fire_event('device-list-change:block')
+
     def device_get(self, vm, ident):
     def device_get(self, vm, ident):
         # pylint: disable=no-self-use
         # pylint: disable=no-self-use
         '''Read information about device from QubesDB
         '''Read information about device from QubesDB
@@ -114,7 +126,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
         :param ident: device identifier
         :param ident: device identifier
         :returns BlockDevice'''
         :returns BlockDevice'''
 
 
-        untrusted_qubes_device_attrs = vm.qdb.list(
+        untrusted_qubes_device_attrs = vm.untrusted_qdb.list(
             '/qubes-block-devices/{}/'.format(ident))
             '/qubes-block-devices/{}/'.format(ident))
         if not untrusted_qubes_device_attrs:
         if not untrusted_qubes_device_attrs:
             return None
             return None
@@ -124,12 +136,11 @@ class BlockDeviceExtension(qubes.ext.Extension):
     def on_device_list_block(self, vm, event):
     def on_device_list_block(self, vm, event):
         # pylint: disable=unused-argument,no-self-use
         # pylint: disable=unused-argument,no-self-use
 
 
-        safe_set = {ord(c) for c in
-            string.ascii_letters + string.digits}
+        safe_set = string.ascii_letters + string.digits
         if not vm.is_running():
         if not vm.is_running():
             return
             return
-        untrusted_qubes_devices = vm.qdb.list('/qubes-block-devices/')
-        untrusted_idents = set(untrusted_path.split(b'/', 3)[2]
+        untrusted_qubes_devices = vm.untrusted_qdb.list('/qubes-block-devices/')
+        untrusted_idents = set(untrusted_path.split('/', 3)[2]
             for untrusted_path in untrusted_qubes_devices)
             for untrusted_path in untrusted_qubes_devices)
         for untrusted_ident in untrusted_idents:
         for untrusted_ident in untrusted_idents:
             if not all(c in safe_set for c in untrusted_ident):
             if not all(c in safe_set for c in untrusted_ident):
@@ -138,7 +149,7 @@ class BlockDeviceExtension(qubes.ext.Extension):
                 vm.log.warning(msg % vm.name)
                 vm.log.warning(msg % vm.name)
                 continue
                 continue
 
 
-            ident = untrusted_ident.decode('ascii', errors='strict')
+            ident = untrusted_ident
 
 
             device_info = self.device_get(vm, ident)
             device_info = self.device_get(vm, ident)
             if device_info:
             if device_info:

+ 10 - 8
qubes/ext/r3compatibility.py

@@ -60,9 +60,9 @@ class R3Compatibility(qubes.ext.Extension):
             vmtype = 'NetVM'
             vmtype = 'NetVM'
         else:
         else:
             vmtype = 'AppVM'
             vmtype = 'AppVM'
-        vm.qdb.write('/qubes-vm-type', vmtype)
+        vm.untrusted_qdb.write('/qubes-vm-type', vmtype)
 
 
-        vm.qdb.write("/qubes-iptables-error", '')
+        vm.untrusted_qdb.write("/qubes-iptables-error", '')
         self.write_iptables_qubesdb_entry(vm)
         self.write_iptables_qubesdb_entry(vm)
 
 
         self.write_services(vm)
         self.write_services(vm)
@@ -81,7 +81,7 @@ class R3Compatibility(qubes.ext.Extension):
 
 
     def write_iptables_qubesdb_entry(self, firewallvm):
     def write_iptables_qubesdb_entry(self, firewallvm):
         # pylint: disable=no-self-use
         # pylint: disable=no-self-use
-        firewallvm.qdb.rm("/qubes-iptables-domainrules/")
+        firewallvm.untrusted_qdb.rm("/qubes-iptables-domainrules/")
         iptables = "# Generated by Qubes Core on {0}\n".format(
         iptables = "# Generated by Qubes Core on {0}\n".format(
             datetime.datetime.now().ctime())
             datetime.datetime.now().ctime())
         iptables += "*filter\n"
         iptables += "*filter\n"
@@ -102,7 +102,7 @@ class R3Compatibility(qubes.ext.Extension):
         # Deny inter-VMs networking
         # Deny inter-VMs networking
         iptables += "-A FORWARD -i vif+ -o vif+ -j DROP\n"
         iptables += "-A FORWARD -i vif+ -o vif+ -j DROP\n"
         iptables += "COMMIT\n"
         iptables += "COMMIT\n"
-        firewallvm.qdb.write("/qubes-iptables-header", iptables)
+        firewallvm.untrusted_qdb.write("/qubes-iptables-header", iptables)
 
 
         for vm in firewallvm.connected_vms:
         for vm in firewallvm.connected_vms:
             iptables = "*filter\n"
             iptables = "*filter\n"
@@ -154,11 +154,12 @@ class R3Compatibility(qubes.ext.Extension):
             iptables += '-A FORWARD -s {0} -j {1}\n'.format(ip,
             iptables += '-A FORWARD -s {0} -j {1}\n'.format(ip,
                 str(conf.policy).upper())
                 str(conf.policy).upper())
             iptables += 'COMMIT\n'
             iptables += 'COMMIT\n'
-            firewallvm.qdb.write('/qubes-iptables-domainrules/' + str(xid),
+            firewallvm.untrusted_qdb.write(
+                '/qubes-iptables-domainrules/' + str(xid),
                 iptables)
                 iptables)
         # no need for ending -A FORWARD -j DROP, cause default action is DROP
         # no need for ending -A FORWARD -j DROP, cause default action is DROP
 
 
-        firewallvm.qdb.write('/qubes-iptables', 'reload')
+        firewallvm.untrusted_qdb.write('/qubes-iptables', 'reload')
 
 
     def write_services(self, vm):
     def write_services(self, vm):
         for feature, value in vm.features.items():
         for feature, value in vm.features.items():
@@ -166,8 +167,9 @@ class R3Compatibility(qubes.ext.Extension):
             if service is None:
             if service is None:
                 continue
                 continue
             # forcefully convert to '0' or '1'
             # forcefully convert to '0' or '1'
-            vm.qdb.write('/qubes-service/{}'.format(service),
+            vm.untrusted_qdb.write('/qubes-service/{}'.format(service),
                 str(int(bool(value))))
                 str(int(bool(value))))
         if 'updates-proxy-setup' in vm.features.keys():
         if 'updates-proxy-setup' in vm.features.keys():
-            vm.qdb.write('/qubes-service/{}'.format('yum-proxy-setup'),
+            vm.untrusted_qdb.write(
+                '/qubes-service/{}'.format('yum-proxy-setup'),
                 str(int(bool(vm.features['updates-proxy-setup']))))
                 str(int(bool(vm.features['updates-proxy-setup']))))

+ 2 - 1
qubes/storage/__init__.py

@@ -419,7 +419,8 @@ class Storage(object):
         # trigger watches to update device status
         # trigger watches to update device status
         # FIXME: this should be removed once libvirt will report such
         # FIXME: this should be removed once libvirt will report such
         # events itself
         # events itself
-        # self.vm.qdb.write('/qubes-block-devices', '') ← do we need this?
+        # self.vm.untrusted_qdb.write('/qubes-block-devices', '')
+        # ← do we need this?
 
 
     def _is_already_attached(self, volume):
     def _is_already_attached(self, volume):
         ''' Checks if the given volume is already attached '''
         ''' Checks if the given volume is already attached '''

+ 22 - 21
qubes/tests/api_misc.py

@@ -37,9 +37,10 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
 
 
     def configure_qdb(self, entries):
     def configure_qdb(self, entries):
         self.src.configure_mock(**{
         self.src.configure_mock(**{
-            'qdb.read.side_effect': (lambda path: entries.get(path, None)),
-            'qdb.list.side_effect': (lambda path:
-                sorted(map(str.encode, entries.keys()))),
+            'untrusted_qdb.read.side_effect': (
+                lambda path: entries.get(path, None)),
+            'untrusted_qdb.list.side_effect': (
+                lambda path: sorted(entries.keys())),
         })
         })
 
 
     def call_mgmt_func(self, method, arg=b'', payload=b''):
     def call_mgmt_func(self, method, arg=b'', payload=b''):
@@ -64,10 +65,10 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             mock.call.save()
             mock.call.save()
         ])
         ])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.list('/features-request/'),
-            mock.call.qdb.read('/features-request/feature1'),
-            mock.call.qdb.read('/features-request/feature2'),
-            mock.call.qdb.read('/features-request/feature3'),
+            mock.call.untrusted_qdb.list('/features-request/'),
+            mock.call.untrusted_qdb.read('/features-request/feature1'),
+            mock.call.untrusted_qdb.read('/features-request/feature2'),
+            mock.call.untrusted_qdb.read('/features-request/feature3'),
             mock.call.fire_event('features-request', untrusted_features={
             mock.call.fire_event('features-request', untrusted_features={
                 'feature1': '1', 'feature2': '', 'feature3': 'other'})
                 'feature1': '1', 'feature2': '', 'feature3': 'other'})
         ])
         ])
@@ -80,7 +81,7 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             mock.call.save()
             mock.call.save()
         ])
         ])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.list('/features-request/'),
+            mock.call.untrusted_qdb.list('/features-request/'),
             mock.call.fire_event('features-request', untrusted_features={})
             mock.call.fire_event('features-request', untrusted_features={})
         ])
         ])
 
 
@@ -93,8 +94,8 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             self.call_mgmt_func(b'qubes.FeaturesRequest')
             self.call_mgmt_func(b'qubes.FeaturesRequest')
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.list('/features-request/'),
-            mock.call.qdb.read('/features-request/feature1'),
+            mock.call.untrusted_qdb.list('/features-request/'),
+            mock.call.untrusted_qdb.read('/features-request/feature1'),
         ])
         ])
 
 
     def test_003_features_request_invalid2(self):
     def test_003_features_request_invalid2(self):
@@ -106,8 +107,8 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             self.call_mgmt_func(b'qubes.FeaturesRequest')
             self.call_mgmt_func(b'qubes.FeaturesRequest')
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.list('/features-request/'),
-            mock.call.qdb.read('/features-request/feature1'),
+            mock.call.untrusted_qdb.list('/features-request/'),
+            mock.call.untrusted_qdb.read('/features-request/feature1'),
         ])
         ])
 
 
     def test_010_notify_tools(self):
     def test_010_notify_tools(self):
@@ -125,9 +126,9 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             mock.call.save()
             mock.call.save()
         ])
         ])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.read('/qubes-tools/qrexec'),
-            mock.call.qdb.read('/qubes-tools/gui'),
-            mock.call.qdb.read('/qubes-tools/default-user'),
+            mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
+            mock.call.untrusted_qdb.read('/qubes-tools/gui'),
+            mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
             mock.call.fire_event('features-request', untrusted_features={
             mock.call.fire_event('features-request', untrusted_features={
                 'gui': '1',
                 'gui': '1',
                 'default-user': 'user',
                 'default-user': 'user',
@@ -146,9 +147,9 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
         response = self.call_mgmt_func(b'qubes.NotifyTools')
         response = self.call_mgmt_func(b'qubes.NotifyTools')
         self.assertIsNone(response)
         self.assertIsNone(response)
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.read('/qubes-tools/qrexec'),
-            mock.call.qdb.read('/qubes-tools/gui'),
-            mock.call.qdb.read('/qubes-tools/default-user'),
+            mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
+            mock.call.untrusted_qdb.read('/qubes-tools/gui'),
+            mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
             mock.call.fire_event('features-request', untrusted_features={
             mock.call.fire_event('features-request', untrusted_features={
                 'gui': '1',
                 'gui': '1',
                 'default-user': 'user',
                 'default-user': 'user',
@@ -169,7 +170,7 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             self.call_mgmt_func(b'qubes.NotifyTools')
             self.call_mgmt_func(b'qubes.NotifyTools')
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.read('/qubes-tools/qrexec'),
+            mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
         ])
         ])
 
 
     def test_016_notify_tools_invalid_value_gui(self):
     def test_016_notify_tools_invalid_value_gui(self):
@@ -185,8 +186,8 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
             self.call_mgmt_func(b'qubes.NotifyTools')
             self.call_mgmt_func(b'qubes.NotifyTools')
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.app.mock_calls, [])
         self.assertEqual(self.src.mock_calls, [
         self.assertEqual(self.src.mock_calls, [
-            mock.call.qdb.read('/qubes-tools/qrexec'),
-            mock.call.qdb.read('/qubes-tools/gui'),
+            mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
+            mock.call.untrusted_qdb.read('/qubes-tools/gui'),
         ])
         ])
 
 
     def test_020_notify_updates_standalone(self):
     def test_020_notify_updates_standalone(self):

+ 76 - 80
qubes/tests/devices_block.py

@@ -94,13 +94,9 @@ class TestQubesDB(object):
         self._data = data
         self._data = data
 
 
     def read(self, key):
     def read(self, key):
-        if isinstance(key, str):
-            key = key.encode()
         return self._data.get(key, None)
         return self._data.get(key, None)
 
 
     def list(self, prefix):
     def list(self, prefix):
-        if isinstance(prefix, str):
-            prefix = prefix.encode()
         return [key for key in self._data if key.startswith(prefix)]
         return [key for key in self._data if key.startswith(prefix)]
 
 
 
 
@@ -120,7 +116,7 @@ class TestApp(object):
 class TestVM(object):
 class TestVM(object):
     def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
     def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
         self.name = name
         self.name = name
-        self.qdb = TestQubesDB(qdb)
+        self.untrusted_qdb = TestQubesDB(qdb)
         self.libvirt_domain = mock.Mock()
         self.libvirt_domain = mock.Mock()
         self.is_running = lambda: running
         self.is_running = lambda: running
         self.log = mock.Mock()
         self.log = mock.Mock()
@@ -143,10 +139,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_000_device_get(self):
     def test_000_device_get(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         device_info = self.ext.device_get(vm, 'sda')
         device_info = self.ext.device_get(vm, 'sda')
         self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
         self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
@@ -161,10 +157,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_001_device_get_other_node(self):
     def test_001_device_get_other_node(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/mapper_dmroot': b'',
-            b'/qubes-block-devices/mapper_dmroot/desc': b'Test device',
-            b'/qubes-block-devices/mapper_dmroot/size': b'1024000',
-            b'/qubes-block-devices/mapper_dmroot/mode': b'w',
+            '/qubes-block-devices/mapper_dmroot': b'',
+            '/qubes-block-devices/mapper_dmroot/desc': b'Test device',
+            '/qubes-block-devices/mapper_dmroot/size': b'1024000',
+            '/qubes-block-devices/mapper_dmroot/mode': b'w',
         })
         })
         device_info = self.ext.device_get(vm, 'mapper_dmroot')
         device_info = self.ext.device_get(vm, 'mapper_dmroot')
         self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
         self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
@@ -179,20 +175,20 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_002_device_get_invalid_desc(self):
     def test_002_device_get_invalid_desc(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device<>za\xc4\x87abc',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device<>za\xc4\x87abc',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         device_info = self.ext.device_get(vm, 'sda')
         device_info = self.ext.device_get(vm, 'sda')
         self.assertEqual(device_info.description, 'Test device__za__abc')
         self.assertEqual(device_info.description, 'Test device__za__abc')
 
 
     def test_003_device_get_invalid_size(self):
     def test_003_device_get_invalid_size(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000abc',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000abc',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         device_info = self.ext.device_get(vm, 'sda')
         device_info = self.ext.device_get(vm, 'sda')
         self.assertEqual(device_info.size, 0)
         self.assertEqual(device_info.size, 0)
@@ -200,10 +196,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_004_device_get_invalid_mode(self):
     def test_004_device_get_invalid_mode(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'abc',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'abc',
         })
         })
         device_info = self.ext.device_get(vm, 'sda')
         device_info = self.ext.device_get(vm, 'sda')
         self.assertEqual(device_info.mode, 'w')
         self.assertEqual(device_info.mode, 'w')
@@ -211,24 +207,24 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_005_device_get_none(self):
     def test_005_device_get_none(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         device_info = self.ext.device_get(vm, 'sdb')
         device_info = self.ext.device_get(vm, 'sdb')
         self.assertIsNone(device_info)
         self.assertIsNone(device_info)
 
 
     def test_010_devices_list(self):
     def test_010_devices_list(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
-            b'/qubes-block-devices/sdb': b'',
-            b'/qubes-block-devices/sdb/desc': b'Test device2',
-            b'/qubes-block-devices/sdb/size': b'2048000',
-            b'/qubes-block-devices/sdb/mode': b'r',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sdb': b'',
+            '/qubes-block-devices/sdb/desc': b'Test device2',
+            '/qubes-block-devices/sdb/size': b'2048000',
+            '/qubes-block-devices/sdb/mode': b'r',
         })
         })
         devices = sorted(list(self.ext.on_device_list_block(vm, '')))
         devices = sorted(list(self.ext.on_device_list_block(vm, '')))
         self.assertEqual(len(devices), 2)
         self.assertEqual(len(devices), 2)
@@ -250,9 +246,9 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_012_devices_list_invalid_ident(self):
     def test_012_devices_list_invalid_ident(self):
         vm = TestVM({
         vm = TestVM({
-            b'/qubes-block-devices/invalid ident': b'',
-            b'/qubes-block-devices/invalid+ident': b'',
-            b'/qubes-block-devices/invalid#': b'',
+            '/qubes-block-devices/invalid ident': b'',
+            '/qubes-block-devices/invalid+ident': b'',
+            '/qubes-block-devices/invalid#': b'',
         })
         })
         devices = sorted(list(self.ext.on_device_list_block(vm, '')))
         devices = sorted(list(self.ext.on_device_list_block(vm, '')))
         self.assertEqual(len(devices), 0)
         self.assertEqual(len(devices), 0)
@@ -334,10 +330,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_040_attach(self):
     def test_040_attach(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -353,10 +349,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_041_attach_frontend(self):
     def test_041_attach_frontend(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -373,10 +369,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_042_attach_read_only(self):
     def test_042_attach_read_only(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -394,10 +390,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_043_attach_invalid_option(self):
     def test_043_attach_invalid_option(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -408,10 +404,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_044_attach_invalid_option2(self):
     def test_044_attach_invalid_option2(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -422,10 +418,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_045_attach_backend_not_running(self):
     def test_045_attach_backend_not_running(self):
         back_vm = TestVM(name='sys-usb', running=False, qdb={
         back_vm = TestVM(name='sys-usb', running=False, qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'w',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'w',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -435,10 +431,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_046_attach_ro_dev_rw(self):
     def test_046_attach_ro_dev_rw(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'r',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'r',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -449,10 +445,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_047_attach_read_only_auto(self):
     def test_047_attach_read_only_auto(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'r',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'r',
         })
         })
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         vm = TestVM({}, domain_xml=domain_xml_template.format(''))
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
         dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -469,10 +465,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_050_detach(self):
     def test_050_detach(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'r',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'r',
         })
         })
         device_xml = (
         device_xml = (
             '<disk type="block" device="disk">\n'
             '<disk type="block" device="disk">\n'
@@ -491,10 +487,10 @@ class TC_00_Block(qubes.tests.QubesTestCase):
 
 
     def test_051_detach_not_attached(self):
     def test_051_detach_not_attached(self):
         back_vm = TestVM(name='sys-usb', qdb={
         back_vm = TestVM(name='sys-usb', qdb={
-            b'/qubes-block-devices/sda': b'',
-            b'/qubes-block-devices/sda/desc': b'Test device',
-            b'/qubes-block-devices/sda/size': b'1024000',
-            b'/qubes-block-devices/sda/mode': b'r',
+            '/qubes-block-devices/sda': b'',
+            '/qubes-block-devices/sda/desc': b'Test device',
+            '/qubes-block-devices/sda/size': b'1024000',
+            '/qubes-block-devices/sda/mode': b'r',
         })
         })
         device_xml = (
         device_xml = (
             '<disk type="block" device="disk">\n'
             '<disk type="block" device="disk">\n'

+ 14 - 0
qubes/tests/integ/basic.py

@@ -65,6 +65,20 @@ class TC_00_Basic(qubes.tests.SystemTestCase):
         with self.assertNotRaises(qubes.exc.QubesException):
         with self.assertNotRaises(qubes.exc.QubesException):
             vm.storage.verify()
             vm.storage.verify()
 
 
+    def test_040_qdb_watch(self):
+        flag = set()
+
+        def handler(vm, event, path):
+            if path == '/test-watch-path':
+                flag.add(True)
+
+        vm = self.app.domains[0]
+        vm.watch_qdb_path('/test-watch-path')
+        vm.add_handler('domain-qdb-change:/test-watch-path', handler)
+        self.assertFalse(flag)
+        vm.untrusted_qdb.write('/test-watch-path', 'test-value')
+        self.loop.run_until_complete(asyncio.sleep(0.1))
+        self.assertTrue(flag)
 
 
 class TC_01_Properties(qubes.tests.SystemTestCase):
 class TC_01_Properties(qubes.tests.SystemTestCase):
     # pylint: disable=attribute-defined-outside-init
     # pylint: disable=attribute-defined-outside-init

+ 52 - 6
qubes/vm/__init__.py

@@ -24,14 +24,9 @@
 '''Qubes Virtual Machines
 '''Qubes Virtual Machines
 
 
 '''
 '''
-
-import datetime
-import os
+import asyncio
 import re
 import re
 import string
 import string
-import subprocess
-import sys
-import xml.parsers.expat
 
 
 import lxml.etree
 import lxml.etree
 
 
@@ -271,6 +266,8 @@ class BaseVM(qubes.PropertyHolder):
         # self.app must be set before super().__init__, because some property
         # self.app must be set before super().__init__, because some property
         # setters need working .app attribute
         # setters need working .app attribute
         #: mother :py:class:`qubes.Qubes` object
         #: mother :py:class:`qubes.Qubes` object
+        self._qdb_watch_paths = set()
+        self._qdb_connection_watch = None
         self.app = app
         self.app = app
 
 
         super(BaseVM, self).__init__(xml, **kwargs)
         super(BaseVM, self).__init__(xml, **kwargs)
@@ -393,6 +390,55 @@ class BaseVM(qubes.PropertyHolder):
             ]).render(vm=self)
             ]).render(vm=self)
         return domain_config
         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):
 class VMProperty(qubes.property):
     '''Property that is referring to a VM
     '''Property that is referring to a VM

+ 4 - 1
qubes/vm/adminvm.py

@@ -55,6 +55,9 @@ class AdminVM(qubes.vm.BaseVM):
         self._qdb_connection = None
         self._qdb_connection = None
         self._libvirt_domain = None
         self._libvirt_domain = None
 
 
+        if not self.app.vmm.offline_mode:
+            self.start_qdb_watch('dom0')
+
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
 
 
@@ -167,7 +170,7 @@ class AdminVM(qubes.vm.BaseVM):
         return None
         return None
 
 
     @property
     @property
-    def qdb(self):
+    def untrusted_qdb(self):
         '''QubesDB handle for this domain.'''
         '''QubesDB handle for this domain.'''
         if self._qdb_connection is None:
         if self._qdb_connection is None:
             import qubesdb  # pylint: disable=import-error
             import qubesdb  # pylint: disable=import-error

+ 8 - 7
qubes/vm/mix/net.py

@@ -291,12 +291,12 @@ class NetVMMixin(qubes.events.Emitter):
         base_dir = '/qubes-firewall/' + vm.ip + '/'
         base_dir = '/qubes-firewall/' + vm.ip + '/'
         # remove old entries if any (but don't touch base empty entry - it
         # remove old entries if any (but don't touch base empty entry - it
         # would trigger reload right away
         # would trigger reload right away
-        self.qdb.rm(base_dir)
+        self.untrusted_qdb.rm(base_dir)
         # write new rules
         # write new rules
         for key, value in vm.firewall.qdb_entries(addr_family=4).items():
         for key, value in vm.firewall.qdb_entries(addr_family=4).items():
-            self.qdb.write(base_dir + key, value)
+            self.untrusted_qdb.write(base_dir + key, value)
         # signal its done
         # signal its done
-        self.qdb.write(base_dir[:-1], '')
+        self.untrusted_qdb.write(base_dir[:-1], '')
 
 
     def set_mapped_ip_info_for_vm(self, vm):
     def set_mapped_ip_info_for_vm(self, vm):
         '''
         '''
@@ -307,14 +307,15 @@ class NetVMMixin(qubes.events.Emitter):
         # add info about remapped IPs (VM IP hidden from the VM itself)
         # add info about remapped IPs (VM IP hidden from the VM itself)
         mapped_ip_base = '/mapped-ip/{}'.format(vm.ip)
         mapped_ip_base = '/mapped-ip/{}'.format(vm.ip)
         if vm.visible_ip:
         if vm.visible_ip:
-            self.qdb.write(mapped_ip_base + '/visible-ip', vm.visible_ip)
+            self.untrusted_qdb.write(mapped_ip_base + '/visible-ip',
+                vm.visible_ip)
         else:
         else:
-            self.qdb.rm(mapped_ip_base + '/visible-ip')
+            self.untrusted_qdb.rm(mapped_ip_base + '/visible-ip')
         if vm.visible_gateway:
         if vm.visible_gateway:
-            self.qdb.write(mapped_ip_base + '/visible-gateway',
+            self.untrusted_qdb.write(mapped_ip_base + '/visible-gateway',
                 vm.visible_gateway)
                 vm.visible_gateway)
         else:
         else:
-            self.qdb.rm(mapped_ip_base + '/visible-gateway')
+            self.untrusted_qdb.rm(mapped_ip_base + '/visible-gateway')
 
 
 
 
     @qubes.events.handler('property-del:netvm')
     @qubes.events.handler('property-del:netvm')

+ 41 - 25
qubes/vm/qubesvm.py

@@ -26,6 +26,8 @@ from __future__ import absolute_import
 import asyncio
 import asyncio
 import base64
 import base64
 import datetime
 import datetime
+import errno
+import grp
 import os
 import os
 import os.path
 import os.path
 import shutil
 import shutil
@@ -34,11 +36,8 @@ import subprocess
 import uuid
 import uuid
 import warnings
 import warnings
 
 
-import grp
-
-import errno
-import lxml
 import libvirt  # pylint: disable=import-error
 import libvirt  # pylint: disable=import-error
+import lxml
 
 
 import qubes
 import qubes
 import qubes.config
 import qubes.config
@@ -249,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.
             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)
         .. event:: backup-get-files (subject, event)
 
 
             Collects additional file to be included in a backup.
             Collects additional file to be included in a backup.
@@ -590,7 +601,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
                 yield block_dev
                 yield block_dev
 
 
     @property
     @property
-    def qdb(self):
+    def untrusted_qdb(self):
         '''QubesDB handle for this domain.'''
         '''QubesDB handle for this domain.'''
         if self._qdb_connection is None:
         if self._qdb_connection is None:
             if self.is_running():
             if self.is_running():
@@ -727,6 +738,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
         if self.storage is None:
         if self.storage is None:
             self.storage = qubes.storage.Storage(self)
             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')
     @qubes.events.handler('property-set:label')
     def on_property_set_label(self, event, name, newvalue, oldvalue=None):
     def on_property_set_label(self, event, name, newvalue, oldvalue=None):
         # pylint: disable=unused-argument
         # pylint: disable=unused-argument
@@ -1714,53 +1728,53 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
         '''
         '''
         # pylint: disable=no-member
         # pylint: disable=no-member
 
 
-        self.qdb.write('/name', self.name)
-        self.qdb.write('/type', self.__class__.__name__)
-        self.qdb.write('/qubes-vm-updateable', str(self.updateable))
-        self.qdb.write('/qubes-vm-persistence',
+        self.untrusted_qdb.write('/name', self.name)
+        self.untrusted_qdb.write('/type', self.__class__.__name__)
+        self.untrusted_qdb.write('/qubes-vm-updateable', str(self.updateable))
+        self.untrusted_qdb.write('/qubes-vm-persistence',
             'full' if self.updateable else 'rw-only')
             'full' if self.updateable else 'rw-only')
-        self.qdb.write('/qubes-debug-mode', str(int(self.debug)))
+        self.untrusted_qdb.write('/qubes-debug-mode', str(int(self.debug)))
         try:
         try:
-            self.qdb.write('/qubes-base-template', self.template.name)
+            self.untrusted_qdb.write('/qubes-base-template', self.template.name)
         except AttributeError:
         except AttributeError:
-            self.qdb.write('/qubes-base-template', '')
+            self.untrusted_qdb.write('/qubes-base-template', '')
 
 
-        self.qdb.write('/qubes-random-seed',
+        self.untrusted_qdb.write('/qubes-random-seed',
             base64.b64encode(qubes.utils.urandom(64)))
             base64.b64encode(qubes.utils.urandom(64)))
 
 
         if self.provides_network:
         if self.provides_network:
             # '/qubes-netvm-network' value is only checked for being non empty
             # '/qubes-netvm-network' value is only checked for being non empty
-            self.qdb.write('/qubes-netvm-network', self.gateway)
-            self.qdb.write('/qubes-netvm-gateway', self.gateway)
-            self.qdb.write('/qubes-netvm-netmask', self.netmask)
+            self.untrusted_qdb.write('/qubes-netvm-network', self.gateway)
+            self.untrusted_qdb.write('/qubes-netvm-gateway', self.gateway)
+            self.untrusted_qdb.write('/qubes-netvm-netmask', self.netmask)
 
 
             for i, addr in zip(('primary', 'secondary'), self.dns):
             for i, addr in zip(('primary', 'secondary'), self.dns):
-                self.qdb.write('/qubes-netvm-{}-dns'.format(i), addr)
+                self.untrusted_qdb.write('/qubes-netvm-{}-dns'.format(i), addr)
 
 
         if self.netvm is not None:
         if self.netvm is not None:
-            self.qdb.write('/qubes-ip', self.visible_ip)
-            self.qdb.write('/qubes-netmask', self.visible_netmask)
-            self.qdb.write('/qubes-gateway', self.visible_gateway)
+            self.untrusted_qdb.write('/qubes-ip', self.visible_ip)
+            self.untrusted_qdb.write('/qubes-netmask', self.visible_netmask)
+            self.untrusted_qdb.write('/qubes-gateway', self.visible_gateway)
 
 
             for i, addr in zip(('primary', 'secondary'), self.dns):
             for i, addr in zip(('primary', 'secondary'), self.dns):
-                self.qdb.write('/qubes-{}-dns'.format(i), addr)
+                self.untrusted_qdb.write('/qubes-{}-dns'.format(i), addr)
 
 
 
 
         tzname = qubes.utils.get_timezone()
         tzname = qubes.utils.get_timezone()
         if tzname:
         if tzname:
-            self.qdb.write('/qubes-timezone', tzname)
+            self.untrusted_qdb.write('/qubes-timezone', tzname)
 
 
         for feature, value in self.features.items():
         for feature, value in self.features.items():
             if not feature.startswith('service.'):
             if not feature.startswith('service.'):
                 continue
                 continue
             service = feature[len('service.'):]
             service = feature[len('service.'):]
             # forcefully convert to '0' or '1'
             # forcefully convert to '0' or '1'
-            self.qdb.write('/qubes-service/{}'.format(service),
+            self.untrusted_qdb.write('/qubes-service/{}'.format(service),
                 str(int(bool(value))))
                 str(int(bool(value))))
 
 
-        self.qdb.write('/qubes-block-devices', '')
+        self.untrusted_qdb.write('/qubes-block-devices', '')
 
 
-        self.qdb.write('/qubes-usb-devices', '')
+        self.untrusted_qdb.write('/qubes-usb-devices', '')
 
 
         # TODO: Currently the whole qmemman is quite Xen-specific, so stay with
         # TODO: Currently the whole qmemman is quite Xen-specific, so stay with
         # xenstore for it until decided otherwise
         # xenstore for it until decided otherwise
@@ -1771,6 +1785,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
 
 
         self.fire_event('domain-qdb-create')
         self.fire_event('domain-qdb-create')
 
 
+        self.start_qdb_watch(self.name)
+
     # TODO async; update this in constructor
     # TODO async; update this in constructor
     def _update_libvirt_domain(self):
     def _update_libvirt_domain(self):
         '''Re-initialise :py:attr:`libvirt_domain`.'''
         '''Re-initialise :py:attr:`libvirt_domain`.'''