From 542c81edddf49989bb1dbb9fb06e9b70298f5b40 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Mon, 17 Feb 2025 23:08:06 +0330 Subject: [PATCH] Add the feature to prohibit starting a qube Qube Manager part of preventing qubes with `prohibit-start` from being started. Showing special status icon and disabling start and related buttons. Supplements: https://github.com/QubesOS/qubes-issues/issues/9622 --- icons/ban.svg | 1 + qubesmanager/qube_manager.py | 32 +++++++++++++ qubesmanager/tests/conftest.py | 1 + qubesmanager/tests/test_qube_manager.py | 60 +++++++++++++++++++++++++ resources.qrc | 1 + 5 files changed, 95 insertions(+) create mode 100644 icons/ban.svg diff --git a/icons/ban.svg b/icons/ban.svg new file mode 100644 index 00000000..5ead57ea --- /dev/null +++ b/icons/ban.svg @@ -0,0 +1 @@ + diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py index 1ddb7424..af7c2120 100644 --- a/qubesmanager/qube_manager.py +++ b/qubesmanager/qube_manager.py @@ -103,6 +103,7 @@ def __init__(self): "Halted" : QIcon(":/blank") } self.outdatedIcons = { + "blocked" : QIcon(":/ban"), "update" : QIcon(":/updateable"), "outdated" : QIcon(":/outdated"), "to-be-outdated" : QIcon(":/outdated"), @@ -110,6 +111,8 @@ def __init__(self): "skipped": QIcon(':/skipped') } self.outdatedTooltips = { + "blocked" : self.tr( + "The qube is prohibited from starting"), "update" : self.tr("Updates available"), "outdated" : self.tr( "The qube must be restarted for recent changes in " @@ -239,6 +242,12 @@ def update_power_state(self): self.state['outdated'] = "" try: + if self.vm.klass != "AdminVM" and manager_utils.get_feature( + self.vm, 'prohibit-start', False): + # Special case where being outdated, eol & skipped is irrelevant + self.state['outdated'] = 'blocked' + return + if manager_utils.is_running(self.vm, False): if hasattr(self.vm, 'template') and \ manager_utils.is_running(self.vm.template, False): @@ -264,6 +273,8 @@ def update_power_state(self): eol = datetime.strptime(eol_string, '%Y-%m-%d') if datetime.now() > eol: self.state['outdated'] = 'eol' + else: + self.state['outdated'] = None except exc.QubesDaemonAccessError: pass @@ -879,6 +890,10 @@ def __init__(self, qt_app, qubes_app, dispatcher, _parent=None): self.on_domain_updates_available) dispatcher.add_handler('domain-feature-delete:skip-update', self.on_domain_updates_available) + dispatcher.add_handler('domain-feature-set:prohibit-start', + self.on_domain_updates_available) + dispatcher.add_handler('domain-feature-delete:prohibit-start', + self.on_domain_updates_available) self.installEventFilter(self) @@ -1125,11 +1140,16 @@ def check_updates(self, info=None): try: if info.vm.klass in {'TemplateVM', 'StandaloneVM'}: if manager_utils.get_feature( + info.vm, 'prohibit-start', False): + info.state['outdated'] = 'blocked' + elif manager_utils.get_feature( info.vm, 'skip-update', False): info.state['outdated'] = 'skipped' elif manager_utils.get_feature( info.vm, 'updates-available', False): info.state['outdated'] = 'update' + else: + info.state['outdated'] = None except exc.QubesDaemonAccessError: return @@ -1352,6 +1372,18 @@ def table_selection_changed(self): if not vm.updateable and vm.klass != 'AdminVM': self.action_updatevm.setEnabled(False) + if vm.state['power'] == 'Halted'and \ + vm.vm.features.get('prohibit-start', False): + self.action_open_console.setEnabled(False) + self.action_resumevm.setEnabled(False) + self.action_startvm_tools_install.setEnabled(False) + self.action_pausevm.setEnabled(False) + self.action_restartvm.setEnabled(False) + self.action_killvm.setEnabled(False) + self.action_shutdownvm.setEnabled(False) + self.action_updatevm.setEnabled(False) + self.action_run_command_in_vm.setEnabled(False) + self.update_template_menu() self.update_network_menu() diff --git a/qubesmanager/tests/conftest.py b/qubesmanager/tests/conftest.py index d303e7f8..e39f071b 100644 --- a/qubesmanager/tests/conftest.py +++ b/qubesmanager/tests/conftest.py @@ -27,6 +27,7 @@ def test_qubes_app(): test_qapp = MockQubesComplete() test_qapp._qubes['sys-usb'].features[ 'supported-feature.keyboard-layout'] = '1' + test_qapp._qubes['sys-usb'].features['prohibit-start'] = None test_qapp.update_vm_calls() return test_qapp diff --git a/qubesmanager/tests/test_qube_manager.py b/qubesmanager/tests/test_qube_manager.py index d9a93385..af983bb3 100644 --- a/qubesmanager/tests/test_qube_manager.py +++ b/qubesmanager/tests/test_qube_manager.py @@ -1604,3 +1604,63 @@ def test_704_check_later(mock_timer, mock_question): assert mock_question.call_count == 0 assert mock_timer.call_count == 1 + + +@pytest.mark.asyncio(loop_scope="module") +async def test_705_prohibit_start_vms(qubes_manager): + # Change `prohibit-start` feature for two qubes + prohibition = ( + 'test-old', + 'admin.vm.feature.Set', + 'prohibit-start', + b'Compromised by Gremlins!', + ) + assert prohibition not in qubes_manager.qubes_app.actual_calls + qubes_manager.qubes_app.expected_calls[prohibition] = b'0\x00' + + prohibition = ( + 'test-standalone', + 'admin.vm.feature.Set', + 'prohibit-start', + b'Do not update', + ) + assert prohibition not in qubes_manager.qubes_app.actual_calls + qubes_manager.qubes_app.expected_calls[prohibition] = b'0\x00' + + qubes_manager.qubes_app._qubes['test-old'].features[ \ + 'prohibit-start'] = 'Compromised by Gremlins!' + qubes_manager.qubes_app._qubes['test-standalone'].features[ \ + 'prohibit-start'] = 'Do not update' + + qubes_manager.qubes_app.update_vm_calls() + qubes_manager.dispatcher.add_expected_event( + MockEvent('test-old', + 'domain-feature-set', + [('name', 'prohibit-start'), + ('newvalue', 'Compromised by Gremlins!')])) + qubes_manager.dispatcher.add_expected_event( + MockEvent('test-standalone', + 'domain-feature-set', + [('name', 'prohibit-start'), + ('newvalue', 'Do not update')])) + + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(qubes_manager.dispatcher.listen_for_events(), 1) + + qubes_manager.qubes_app.domains['test-old'].features[ \ + 'prohibit-start'] = \ + 'Compromised by Gremlins!' + qubes_manager.qubes_app.domains['test-standalone'].features[ \ + 'prohibit-start'] = \ + 'Do not update' + for vm in ['test-old', 'test-standalone']: + _select_vm(qubes_manager, vm) + assert not qubes_manager.action_open_console.isEnabled() + assert not qubes_manager.action_resumevm.isEnabled() + assert not qubes_manager.action_startvm_tools_install.isEnabled() + assert not qubes_manager.action_pausevm.isEnabled() + assert not qubes_manager.action_restartvm.isEnabled() + assert not qubes_manager.action_killvm.isEnabled() + assert not qubes_manager.action_shutdownvm.isEnabled() + assert not qubes_manager.action_updatevm.isEnabled() + assert not qubes_manager.action_run_command_in_vm.isEnabled() diff --git a/resources.qrc b/resources.qrc index 9c4d87d2..524d6496 100644 --- a/resources.qrc +++ b/resources.qrc @@ -3,6 +3,7 @@ icons/add.svg icons/apps.svg icons/backup.svg + icons/ban.svg icons/blank.svg icons/checked.svg icons/checkmark.svg