Skip to content

Commit 9f34b17

Browse files
committed
Add the feature to prohibit starting a qube
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
1 parent 334419b commit 9f34b17

File tree

5 files changed

+89
-0
lines changed

5 files changed

+89
-0
lines changed

icons/ban.svg

Lines changed: 1 addition & 0 deletions
Loading

qubesmanager/qube_manager.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,16 @@ def __init__(self):
103103
"Halted" : QIcon(":/blank")
104104
}
105105
self.outdatedIcons = {
106+
"blocked" : QIcon(":/ban"),
106107
"update" : QIcon(":/updateable"),
107108
"outdated" : QIcon(":/outdated"),
108109
"to-be-outdated" : QIcon(":/outdated"),
109110
"eol": QIcon(':/warning'),
110111
"skipped": QIcon(':/skipped')
111112
}
112113
self.outdatedTooltips = {
114+
"blocked" : self.tr(
115+
"The qube is prohibited from being started"),
113116
"update" : self.tr("Updates available"),
114117
"outdated" : self.tr(
115118
"The qube must be restarted for recent changes in "
@@ -239,6 +242,12 @@ def update_power_state(self):
239242

240243
self.state['outdated'] = ""
241244
try:
245+
if self.vm.klass != "AdminVM" and manager_utils.get_feature(
246+
self.vm, 'prohibit-start', False):
247+
# Special case where being outdated, eol & skipped is irrelevant
248+
self.state['outdated'] = 'blocked'
249+
return
250+
242251
if manager_utils.is_running(self.vm, False):
243252
if hasattr(self.vm, 'template') and \
244253
manager_utils.is_running(self.vm.template, False):
@@ -253,6 +262,9 @@ def update_power_state(self):
253262

254263
if self.vm.klass in {'TemplateVM', 'StandaloneVM'}:
255264
if manager_utils.get_feature(
265+
self.vm, 'prohibit-start', False):
266+
self.state['outdated'] = 'ban'
267+
elif manager_utils.get_feature(
256268
self.vm, 'skip-update', False):
257269
self.state['outdated'] = 'skipped'
258270
elif manager_utils.get_feature(
@@ -264,6 +276,8 @@ def update_power_state(self):
264276
eol = datetime.strptime(eol_string, '%Y-%m-%d')
265277
if datetime.now() > eol:
266278
self.state['outdated'] = 'eol'
279+
else:
280+
self.state['outdated'] = None
267281
except exc.QubesDaemonAccessError:
268282
pass
269283

@@ -879,6 +893,10 @@ def __init__(self, qt_app, qubes_app, dispatcher, _parent=None):
879893
self.on_domain_updates_available)
880894
dispatcher.add_handler('domain-feature-delete:skip-update',
881895
self.on_domain_updates_available)
896+
dispatcher.add_handler('domain-feature-set:prohibit-start',
897+
self.on_domain_updates_available)
898+
dispatcher.add_handler('domain-feature-delete:prohibit-start',
899+
self.on_domain_updates_available)
882900

883901
self.installEventFilter(self)
884902

@@ -1125,11 +1143,16 @@ def check_updates(self, info=None):
11251143
try:
11261144
if info.vm.klass in {'TemplateVM', 'StandaloneVM'}:
11271145
if manager_utils.get_feature(
1146+
info.vm, 'prohibit-start', False):
1147+
info.state['outdated'] = 'blocked'
1148+
elif manager_utils.get_feature(
11281149
info.vm, 'skip-update', False):
11291150
info.state['outdated'] = 'skipped'
11301151
elif manager_utils.get_feature(
11311152
info.vm, 'updates-available', False):
11321153
info.state['outdated'] = 'update'
1154+
else:
1155+
info.state['outdated'] = None
11331156
except exc.QubesDaemonAccessError:
11341157
return
11351158

@@ -1352,6 +1375,17 @@ def table_selection_changed(self):
13521375
if not vm.updateable and vm.klass != 'AdminVM':
13531376
self.action_updatevm.setEnabled(False)
13541377

1378+
if vm.vm.features.get('prohibit-start', False):
1379+
self.action_open_console.setEnabled(False)
1380+
self.action_resumevm.setEnabled(False)
1381+
self.action_startvm_tools_install.setEnabled(False)
1382+
self.action_pausevm.setEnabled(False)
1383+
self.action_restartvm.setEnabled(False)
1384+
self.action_killvm.setEnabled(False)
1385+
self.action_shutdownvm.setEnabled(False)
1386+
self.action_updatevm.setEnabled(False)
1387+
self.action_run_command_in_vm.setEnabled(False)
1388+
13551389
self.update_template_menu()
13561390
self.update_network_menu()
13571391

qubesmanager/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_qubes_app():
2727
test_qapp = MockQubesComplete()
2828
test_qapp._qubes['sys-usb'].features[
2929
'supported-feature.keyboard-layout'] = '1'
30+
test_qapp._qubes['sys-usb'].features['prohibit-start'] = None
3031
test_qapp.update_vm_calls()
3132

3233
return test_qapp

qubesmanager/tests/test_qube_manager.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,3 +1604,55 @@ def test_704_check_later(mock_timer, mock_question):
16041604

16051605
assert mock_question.call_count == 0
16061606
assert mock_timer.call_count == 1
1607+
1608+
1609+
@pytest.mark.asyncio(loop_scope="module")
1610+
async def test_705_prohibit_start_vms(qubes_manager):
1611+
# Change `prohibit-start` feature for two qubes
1612+
prohibition = (
1613+
'test-old',
1614+
'admin.vm.feature.Set',
1615+
'prohibit-start',
1616+
b'Compromised by Gremlins!',
1617+
)
1618+
assert prohibition not in qubes_manager.qubes_app.actual_calls
1619+
qubes_manager.qubes_app.expected_calls[prohibition] = b'0\x00'
1620+
1621+
prohibition = (
1622+
'test-standalone',
1623+
'admin.vm.feature.Set',
1624+
'prohibit-start',
1625+
b'Do not update',
1626+
)
1627+
assert prohibition not in qubes_manager.qubes_app.actual_calls
1628+
qubes_manager.qubes_app.expected_calls[prohibition] = b'0\x00'
1629+
1630+
qubes_manager.qubes_app._qubes['test-old'].features[ \
1631+
'prohibit-start'] = 'Compromised by Gremlins!'
1632+
qubes_manager.qubes_app._qubes['test-standalone'].features[ \
1633+
'prohibit-start'] = 'Do not update'
1634+
1635+
qubes_manager.qubes_app.update_vm_calls()
1636+
qubes_manager.dispatcher.add_expected_event(
1637+
MockEvent('test-old',
1638+
'domain-feature-set',
1639+
[('name', 'prohibit-start'),
1640+
('newvalue', 'Compromised by Gremlins!')]))
1641+
qubes_manager.dispatcher.add_expected_event(
1642+
MockEvent('test-standalone',
1643+
'domain-feature-set',
1644+
[('name', 'prohibit-start'),
1645+
('newvalue', 'Do not update')]))
1646+
1647+
with contextlib.suppress(asyncio.TimeoutError):
1648+
await asyncio.wait_for(qubes_manager.dispatcher.listen_for_events(), 1)
1649+
1650+
qubes_manager.qubes_app.domains['test-old'].features[ \
1651+
'prohibit-start'] = \
1652+
'Compromised by Gremlins!'
1653+
_select_vm(qubes_manager, 'test-old')
1654+
1655+
qubes_manager.qubes_app.domains['test-standalone'].features[ \
1656+
'prohibit-start'] = \
1657+
'Do not update'
1658+
_select_vm(qubes_manager, 'test-standalone')

resources.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<file alias="add">icons/add.svg</file>
44
<file alias="apps">icons/apps.svg</file>
55
<file alias="backup">icons/backup.svg</file>
6+
<file alias="ban">icons/ban.svg</file>
67
<file alias="blank">icons/blank.svg</file>
78
<file alias="checked">icons/checked.svg</file>
89
<file alias="checkmark">icons/checkmark.svg</file>

0 commit comments

Comments
 (0)