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..f6901877 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,17 @@ def table_selection_changed(self):
if not vm.updateable and vm.klass != 'AdminVM':
self.action_updatevm.setEnabled(False)
+ if vm.state['outdated'] == '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..13e16c95 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 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..542cdb8b 100644
--- a/qubesmanager/tests/test_qube_manager.py
+++ b/qubesmanager/tests/test_qube_manager.py
@@ -1604,3 +1604,59 @@ 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()
+
+ with contextlib.suppress(asyncio.TimeoutError):
+ await asyncio.wait_for(qubes_manager.dispatcher.listen_for_events(), 1)
+
+ # Now flip `prohibit-start` feature for two qubes
+ prohibition = (
+ 'test-standalone',
+ 'admin.vm.feature.Set',
+ 'prohibit-start',
+ None,
+ )
+ assert prohibition not in qubes_manager.qubes_app.actual_calls
+ qubes_manager.qubes_app.expected_calls[prohibition] = b'0\x00'
+
+ prohibition = (
+ 'test-red',
+ 'admin.vm.feature.Set',
+ 'prohibit-start',
+ 'Don not start this qube from now on',
+ )
+ 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-standalone'].features[ \
+ 'prohibit-start'] = None
+ qubes_manager.qubes_app._qubes['test-red'].features[ \
+ 'prohibit-start'] = 'Do not start this qube from now on'
+
+ qubes_manager.dispatcher.add_expected_event(
+ MockEvent('test-standalone',
+ 'domain-feature-set',
+ [('name', 'prohibit-start'),
+ ('newvalue', '')]))
+ qubes_manager.dispatcher.add_expected_event(
+ MockEvent('test-red',
+ 'domain-feature-set',
+ [('name', 'prohibit-start'),
+ ('newvalue', 'Do not start this qube from now on')]))
+
+ # 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