From 02cfab8257db78557bfa3d3ce62292062d01fad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 3 Dec 2019 06:16:10 +0100 Subject: [PATCH] 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 --- qubesadmin/base.py | 50 +++++++++++++++++++++++++++++++ qubesadmin/tests/vm/properties.py | 31 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/qubesadmin/base.py b/qubesadmin/base.py index 1b1d6f0..76564aa 100644 --- a/qubesadmin/base.py +++ b/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): ''' diff --git a/qubesadmin/tests/vm/properties.py b/qubesadmin/tests/vm/properties.py index 0a492be..fdc2e9a 100644 --- a/qubesadmin/tests/vm/properties.py +++ b/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):