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..395b8d1e 100644 --- a/qubesmanager/qube_manager.py +++ b/qubesmanager/qube_manager.py @@ -100,7 +100,8 @@ def __init__(self): "Transient" : QIcon(":/transient"), "Halting" : QIcon(":/transient"), "Dying" : QIcon(":/transient"), - "Halted" : QIcon(":/blank") + "Halted" : QIcon(":/blank"), + "Blocked" : QIcon(":/ban"), } self.outdatedIcons = { "update" : QIcon(":/updateable"), @@ -193,8 +194,17 @@ def helpEvent(self, event, view, option, index): # sometimes it's not enough to use an empty string if index != self.lastIndex: QToolTip.showText(QPoint(), ' ') - QToolTip.showText(event.globalPos(), - index.data()['power'], view) + if index.data()['power'] == 'Blocked': + QToolTip.showText(event.globalPos(), + self.tr( + "The qube is prohibited from starting\n" + "See `qvm-features` manual for more information" + ), + view + ) + else: + QToolTip.showText(event.globalPos(), + index.data()['power'], view) else: margin = iconRect.left() - option.rect.left() left = delta = margin + iconRect.width() @@ -234,6 +244,14 @@ def __init__(self, vm): def update_power_state(self): try: self.state['power'] = self.vm.get_power_state() + if self.state['power'] == "Halted" and \ + self.vm.klass != "AdminVM" and \ + manager_utils.get_feature( + self.vm, + 'prohibit-start', + False + ): + self.state['power'] = 'Blocked' except exc.QubesDaemonAccessError: self.state['power'] = "" @@ -264,6 +282,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 @@ -856,6 +876,10 @@ def __init__(self, qt_app, qubes_app, dispatcher, _parent=None): dispatcher.add_handler('domain-shutdown', self.on_domain_status_changed) dispatcher.add_handler('domain-paused', self.on_domain_status_changed) dispatcher.add_handler('domain-unpaused', self.on_domain_status_changed) + dispatcher.add_handler('domain-feature-set:prohibit-start', + self.on_domain_status_changed) + dispatcher.add_handler('domain-feature-delete:prohibit-start', + self.on_domain_status_changed) dispatcher.add_handler('domain-add', self.on_domain_added) dispatcher.add_handler('domain-delete', self.on_domain_removed) @@ -1130,6 +1154,10 @@ def check_updates(self, info=None): elif manager_utils.get_feature( info.vm, 'updates-available', False): info.state['outdated'] = 'update' + else: + info.state['outdated'] = None + else: + info.state['outdated'] = None except exc.QubesDaemonAccessError: return @@ -1352,6 +1380,17 @@ def table_selection_changed(self): if not vm.updateable and vm.klass != 'AdminVM': self.action_updatevm.setEnabled(False) + if vm.state['power'] == 'Blocked': + 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..17487b32 100644 --- a/qubesmanager/tests/conftest.py +++ b/qubesmanager/tests/conftest.py @@ -27,6 +27,8 @@ def test_qubes_app(): test_qapp = MockQubesComplete() test_qapp._qubes['sys-usb'].features[ 'supported-feature.keyboard-layout'] = '1' + test_qapp._qubes['test-standalone'].features['prohibit-start'] = \ + 'Control qube which should be start prohibited from Manager launch' 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..dc60db90 100644 --- a/qubesmanager/tests/test_qube_manager.py +++ b/qubesmanager/tests/test_qube_manager.py @@ -1604,3 +1604,41 @@ 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): + # `prohibit-start` is enabled for `test-standalone` before manager launch + # Flip `prohibit-start` feature for two qubes during Manager running + # Check the status of `start/resume` menu before and after. + + _select_vm(qubes_manager, 'test-standalone') + assert not qubes_manager.action_resumevm.isEnabled() + _select_vm(qubes_manager, 'test-red') + assert qubes_manager.action_resumevm.isEnabled() + + # Now flip `prohibit-start` feature for two qubes + qubes_manager.qubes_app._qubes['test-standalone'].features[ \ + 'prohibit-start'] = '' + qubes_manager.qubes_app._qubes['test-red'].features[ \ + 'prohibit-start'] = 'Do not start this qube from now on' + qubes_manager.qubes_app.update_vm_calls() + + qubes_manager.dispatcher.add_expected_event( + MockEvent('test-standalone', + 'domain-feature-delete:prohibit-start', + [('name', 'prohibit-start')])) + qubes_manager.dispatcher.add_expected_event( + MockEvent('test-red', + 'domain-feature-set:prohibit-start', + [('name', 'prohibit-start'), + ('newvalue', 'Do not start this qube from now on')])) + + with contextlib.suppress(asyncio.TimeoutError): + await asyncio.wait_for(qubes_manager.dispatcher.listen_for_events(), 1) + + # Finally test if their status within Qube Manager is flipped correctly + _select_vm(qubes_manager, 'test-standalone') + assert qubes_manager.action_resumevm.isEnabled() + _select_vm(qubes_manager, 'test-red') + assert not qubes_manager.action_resumevm.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