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