Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the feature to prohibit starting a qube #408

Merged
merged 1 commit into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
45 changes: 42 additions & 3 deletions qubesmanager/qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@
"Transient" : QIcon(":/transient"),
"Halting" : QIcon(":/transient"),
"Dying" : QIcon(":/transient"),
"Halted" : QIcon(":/blank")
"Halted" : QIcon(":/blank"),
"Blocked" : QIcon(":/ban"),
}
self.outdatedIcons = {
"update" : QIcon(":/updateable"),
Expand Down Expand Up @@ -193,8 +194,17 @@
# 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(),

Check warning on line 198 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L197-L198

Added lines #L197 - L198 were not covered by tests
self.tr(
"The qube is prohibited from starting\n"
"See `qvm-features` manual for more information"
),
view
)
else:
QToolTip.showText(event.globalPos(),

Check warning on line 206 in qubesmanager/qube_manager.py

View check run for this annotation

Codecov / codecov/patch

qubesmanager/qube_manager.py#L206

Added line #L206 was not covered by tests
index.data()['power'], view)
else:
margin = iconRect.left() - option.rect.left()
left = delta = margin + iconRect.width()
Expand Down Expand Up @@ -234,6 +244,14 @@
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'] = ""

Expand Down Expand Up @@ -264,6 +282,8 @@
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 @@ -856,6 +876,10 @@
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)
Expand Down Expand Up @@ -1130,6 +1154,10 @@
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

Expand Down Expand Up @@ -1352,6 +1380,17 @@
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()

Expand Down
2 changes: 2 additions & 0 deletions qubesmanager/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 38 additions & 0 deletions qubesmanager/tests/test_qube_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
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