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

* origin/pr/369:
  ext: support for non-service feature advertisement
This commit is contained in:
Marek Marczykowski-Górecki 2020-10-10 03:33:09 +02:00
commit 7ffa7564cf
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
5 changed files with 155 additions and 0 deletions

View File

@ -183,6 +183,30 @@ Services and features can be then inspected from dom0 using
$ qvm-features my-qube $ qvm-features my-qube
supported-service.my-service 1 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 Module contents
--------------- ---------------

View File

@ -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]

View File

@ -395,3 +395,64 @@ class TC_20_Services(qubes.tests.QubesTestCase):
'service.guivm-gui-agent', '') 'service.guivm-gui-agent', '')
self.assertEqual(os.path.exists(service_path), False) 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,
})

View File

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

View File

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