Sfoglia il codice sorgente

Try to use new property.GetAll method to pre-fill the cache

When caching is enabled, reduce number of calls by getting all the
properties at once. If the call is not available (for example because of
the policy), fallback to getting individual values.

QubesOS/qubes-issues#5415
Marek Marczykowski-Górecki 4 anni fa
parent
commit
02cfab8257
2 ha cambiato i file con 81 aggiunte e 0 eliminazioni
  1. 50 0
      qubesadmin/base.py
  2. 31 0
      qubesadmin/tests/vm/properties.py

+ 50 - 0
qubesadmin/base.py

@@ -142,6 +142,9 @@ class PropertyHolder(object):
         '''
         if item.startswith('_'):
             raise AttributeError(item)
+        # pre-fill cache if enabled
+        if self.app.cache_enabled and not self._properties_cache:
+            self._fetch_all_properties()
         # cached value
         if item in self._properties_cache:
             return self._properties_cache[item][0]
@@ -197,6 +200,9 @@ class PropertyHolder(object):
     def __getattr__(self, item):
         if item.startswith('_'):
             raise AttributeError(item)
+        # pre-fill cache if enabled
+        if self.app.cache_enabled and not self._properties_cache:
+            self._fetch_all_properties()
         # cached value
         if item in self._properties_cache:
             value = self._properties_cache[item][1]
@@ -272,6 +278,50 @@ class PropertyHolder(object):
         raise qubesadmin.exc.QubesDaemonCommunicationError(
             'Received invalid value type: {}'.format(prop_type))
 
+    def _fetch_all_properties(self):
+        """
+        Retrieve all properties values at once using (prefix).property.GetAll
+        method. If it succeed, save retrieved values in the properties cache.
+        If the request fails (for example because of qrexec policy), do nothing.
+        Exceptions when parsing received value are not handled.
+
+        :return: None
+        """
+
+        def unescape(line):
+            """Handle \\-escaped values, generates a list of character codes"""
+            escaped = False
+            for char in line:
+                if escaped:
+                    assert char in (ord('n'), ord('\\'))
+                    if char == ord('n'):
+                        yield ord('\n')
+                    elif char == ord('\\'):
+                        yield char
+                    escaped = False
+                elif char == ord('\\'):
+                    escaped = True
+                else:
+                    yield char
+            assert not escaped
+
+        try:
+            properties_str = self.qubesd_call(
+                self._method_dest,
+                self._method_prefix + 'GetAll',
+                None,
+                None)
+        except qubesadmin.exc.QubesDaemonNoResponseError:
+            return
+        for line in properties_str.splitlines():
+            # decode newlines
+            line = bytes(unescape(line))
+            name, property_str = line.split(b' ', 1)
+            name = name.decode()
+            is_default, value = self._deserialize_property(property_str)
+            self._properties_cache[name] = (is_default, value)
+        self._properties = list(self._properties_cache.keys())
+
     @classmethod
     def _local_properties(cls):
         '''

+ 31 - 0
qubesadmin/tests/vm/properties.py

@@ -188,6 +188,37 @@ class TC_00_Properties(qubesadmin.tests.vm.VMTestCase):
             self.vm.property_get_default('prop1')
         self.assertAllCalled()
 
+    def test_050_get_all(self):
+        self.app.expected_calls[
+            ('test-vm', 'admin.vm.property.GetAll', None, None)] = [
+            b'0\x00name default=False type=str test-vm\n'
+            b'debug default=True type=bool False\n'
+            b'backup_timestamp default=True type=int \n'
+            b'kernel default=True type=str 1.0\n'
+            b'qid default=True type=int 3\n'
+            b'kernelopts default=False type=str opt1\\nopt2\\nopt3\\\\opt4\n'
+            b'klass default=True type=str AppVM\n', ]
+        self.app.cache_enabled = True
+        self.assertEqual(self.vm.name, 'test-vm')
+        with self.assertRaises(AttributeError):
+            self.vm.backup_timestamp
+        self.assertEqual(self.vm.debug, False)
+        self.assertEqual(self.vm.qid, 3)
+        self.assertEqual(self.vm.kernelopts, 'opt1\nopt2\nopt3\\opt4')
+        self.assertTrue(self.vm.property_is_default('kernel'))
+
+    def test_051_get_all_fallback(self):
+        self.app.expected_calls[
+            ('test-vm', 'admin.vm.property.GetAll', None, None)] = [b'', ]
+        self.app.expected_calls[
+            ('test-vm', 'admin.vm.property.Get', 'qid', None)] = \
+            b'0\x00default=True type=int 3'
+        self.app.cache_enabled = True
+        self.assertEqual(self.vm.qid, 3)
+        # check if cached
+        self.assertEqual(self.vm.qid, 3)
+        self.assertAllCalled()
+
 
 class TC_01_SpecialCases(qubesadmin.tests.vm.VMTestCase):
     def test_000_get_name(self):