Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/pr/408'
Browse files Browse the repository at this point in the history
* origin/pr/408:
  Add the feature to prohibit starting a qube

Pull request description:

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
marmarek committed Mar 10, 2025
2 parents 6bdf5bd + 601e10b commit 005f4c6
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 3 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.
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 @@ 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"),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'] = ""

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

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

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

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

0 comments on commit 005f4c6

Please sign in to comment.