Browse Source

Merge remote-tracking branch 'origin/pr/369'

* origin/pr/369:
  ext: support for non-service feature advertisement
Marek Marczykowski-Górecki 4 years ago
parent
commit
7ffa7564cf
5 changed files with 155 additions and 0 deletions
  1. 24 0
      doc/qubes-features.rst
  2. 68 0
      qubes/ext/supported_features.py
  3. 61 0
      qubes/tests/ext.py
  4. 1 0
      rpm_spec/core-dom0.spec.in
  5. 1 0
      setup.py

+ 24 - 0
doc/qubes-features.rst

@@ -183,6 +183,30 @@ Services and features can be then inspected from dom0 using
    $ qvm-features my-qube
    supported-service.my-service  1
 
+
+Announcing supported features
+------------------------------
+
+For non-service features, there is similar announce mechanis to the above, but
+uses ``supported-feature.`` prefix. It works like this:
+
+1. The TemplateVM (or StandaloneVM) announces
+``supported-feature.FEATURE_NAME=1`` (with ``FEATURE_NAME`` replaced with an
+actual feature name) using ``qvm-features-request`` tool (see above how to use it).
+2. core-admin extension
+:py:class:`qubes.ext.supported_features.SupportedFeaturesExtension` records
+such requests as features on the same VM.
+3. Any tool that wants to check if the feature support is advertised by the VM,
+can look into its features. For template-based VMs it is advised to check
+also its template - there is a `vm.features.check_with_template()`
+function specifically for this.
+
+Note that (similar to services) it is not necessary for the feature to be
+advertised to enable it. In fact, many features does not need any support from
+the VM side, so they will work without matching ``supported-feature.`` entry.
+Whether a feature requires VM-side support, is documented on case-by-case basis
+in `qvm-features` tool manual page.
+
 Module contents
 ---------------
 

+ 68 - 0
qubes/ext/supported_features.py

@@ -0,0 +1,68 @@
+# -*- encoding: utf-8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2020 Marek Marczykowski-Górecki
+#                               <marmarek@invisiblethingslab.com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, see <https://www.gnu.org/licenses/>.
+
+"""Extension responsible for announcing supported features"""
+
+import qubes.ext
+import qubes.config
+
+# pylint: disable=too-few-public-methods
+
+class SupportedFeaturesExtension(qubes.ext.Extension):
+    """This extension handles VM announcing non-service features as
+        'supported-feature.*' features.
+    """
+
+    @qubes.ext.handler('features-request')
+    def supported_features(self, vm, event, untrusted_features):
+        """Handle advertisement of supported features"""
+        # pylint: disable=no-self-use,unused-argument
+
+        if getattr(vm, 'template', None):
+            vm.log.warning(
+                'Ignoring qubes.FeaturesRequest from template-based VM')
+            return
+
+        new_supported_features = set()
+        for requested_feature in untrusted_features:
+            if not requested_feature.startswith('supported-feature.'):
+                continue
+            if untrusted_features[requested_feature] == '1':
+                # only allow to advertise feature as supported, lack of entry
+                #  means feature is not supported
+                new_supported_features.add(requested_feature)
+        del untrusted_features
+
+        # if no feature is supported, ignore the whole thing - do not clear
+        # all features in case of empty request (manual or such)
+        if not new_supported_features:
+            return
+
+        old_supported_features = set(
+            feat for feat in vm.features
+            if feat.startswith('supported-feature.') and vm.features[feat])
+
+        for feature in new_supported_features.difference(
+                old_supported_features):
+            vm.features[feature] = True
+
+        for feature in old_supported_features.difference(
+                new_supported_features):
+            del vm.features[feature]

+ 61 - 0
qubes/tests/ext.py

@@ -395,3 +395,64 @@ class TC_20_Services(qubes.tests.QubesTestCase):
             'service.guivm-gui-agent', '')
 
         self.assertEqual(os.path.exists(service_path), False)
+
+class TC_30_SupportedFeatures(qubes.tests.QubesTestCase):
+    def setUp(self):
+        super().setUp()
+        self.ext = qubes.ext.supported_features.SupportedFeaturesExtension()
+        self.features = {}
+        specs = {
+            'features.get.side_effect': self.features.get,
+            'features.items.side_effect': self.features.items,
+            'features.__iter__.side_effect': self.features.__iter__,
+            'features.__contains__.side_effect': self.features.__contains__,
+            'features.__setitem__.side_effect': self.features.__setitem__,
+            'features.__delitem__.side_effect': self.features.__delitem__,
+        }
+        vmspecs = {**specs, **{
+            'template': None,
+            'maxmem': 1024,
+            'is_running.return_value': True,
+            }}
+        dom0specs = {**specs, **{
+            'name': "dom0",
+            }}
+        self.vm = mock.MagicMock()
+        self.vm.configure_mock(**vmspecs)
+        self.dom0 = mock.MagicMock()
+        self.dom0.configure_mock(**dom0specs)
+
+    def test_010_supported_features(self):
+        self.ext.supported_features(self.vm, 'features-request',
+            untrusted_features={
+                'supported-feature.test1': '1',  # ok
+                'supported-feature.test2': '0',  # ignored
+                'supported-feature.test3': 'some text',  # ignored
+                'no-feature': '1',  # ignored
+            })
+        self.assertEqual(self.features, {
+            'supported-feature.test1': True,
+        })
+
+    def test_011_supported_features_add(self):
+        self.features['supported-feature.test1'] = '1'
+        self.ext.supported_features(self.vm, 'features-request',
+            untrusted_features={
+                'supported-feature.test1': '1',  # ok
+                'supported-feature.test2': '1',  # ok
+            })
+        # also check if existing one is untouched
+        self.assertEqual(self.features, {
+            'supported-feature.test1': '1',
+            'supported-feature.test2': True,
+        })
+
+    def test_012_supported_features_remove(self):
+        self.features['supported-feature.test1'] = '1'
+        self.ext.supported_features(self.vm, 'features-request',
+            untrusted_features={
+                'supported-feature.test2': '1',  # ok
+            })
+        self.assertEqual(self.features, {
+            'supported-feature.test2': True,
+        })

+ 1 - 0
rpm_spec/core-dom0.spec.in

@@ -425,6 +425,7 @@ done
 %{python3_sitelib}/qubes/ext/pci.py
 %{python3_sitelib}/qubes/ext/r3compatibility.py
 %{python3_sitelib}/qubes/ext/services.py
+%{python3_sitelib}/qubes/ext/supported_features.py
 %{python3_sitelib}/qubes/ext/windows.py
 
 %dir %{python3_sitelib}/qubes/tests

+ 1 - 0
setup.py

@@ -70,6 +70,7 @@ if __name__ == '__main__':
                 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
                 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',
                 'qubes.ext.services = qubes.ext.services:ServicesExtension',
+                'qubes.ext.supported_features = qubes.ext.supported_features:SupportedFeaturesExtension',
                 'qubes.ext.windows = qubes.ext.windows:WindowsFeatures',
             ],
             'qubes.devices': [