Skip to content

Commit

Permalink
Add the feature to prohibit starting a qube
Browse files Browse the repository at this point in the history
Qube Manager part of preventing qubes with `prohibit-start` from being
started. Showing special status icon and disabling start and related
buttons.

Supplements: QubesOS/qubes-issues#9622
  • Loading branch information
alimirjamali committed Feb 27, 2025
1 parent 3636142 commit 542c81e
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 0 deletions.
1 change: 1 addition & 0 deletions icons/ban.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions qubesmanager/qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,16 @@ def __init__(self):
"Halted" : QIcon(":/blank")
}
self.outdatedIcons = {
"blocked" : QIcon(":/ban"),
"update" : QIcon(":/updateable"),
"outdated" : QIcon(":/outdated"),
"to-be-outdated" : QIcon(":/outdated"),
"eol": QIcon(':/warning'),
"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 "
Expand Down Expand Up @@ -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

Check warning on line 249 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L248-L249

Added lines #L248 - L249 were not covered by tests

if manager_utils.is_running(self.vm, False):
if hasattr(self.vm, 'template') and \
manager_utils.is_running(self.vm.template, False):
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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'

Check warning on line 1144 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L1144

Added line #L1144 was not covered by tests
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

Expand Down Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions qubesmanager/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
60 changes: 60 additions & 0 deletions qubesmanager/tests/test_qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<file alias="add">icons/add.svg</file>
<file alias="apps">icons/apps.svg</file>
<file alias="backup">icons/backup.svg</file>
<file alias="ban">icons/ban.svg</file>
<file alias="blank">icons/blank.svg</file>
<file alias="checked">icons/checked.svg</file>
<file alias="checkmark">icons/checkmark.svg</file>
Expand Down

0 comments on commit 542c81e

Please sign in to comment.