Merge remote-tracking branch 'origin/pr/369'
* origin/pr/369: ext: support for non-service feature advertisement
This commit is contained in:
		
						commit
						7ffa7564cf
					
				| @ -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
									
								
								qubes/ext/supported_features.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								qubes/ext/supported_features.py
									
									
									
									
									
										Normal 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] | ||||
| @ -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, | ||||
|         }) | ||||
|  | ||||
| @ -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
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								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': [ | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki