Skip to content

Commit 777796a

Browse files
committed
Mark VMs with prohibit-start feature in updater
Related: QubesOS/qubes-issues#9622
1 parent 7652c60 commit 777796a

File tree

6 files changed

+172
-8
lines changed

6 files changed

+172
-8
lines changed

qui/updater.glade

+7-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<column type="gint"/>
3333
<!-- column-name gchararray1 -->
3434
<column type="PyObject"/>
35+
<!-- column-name rationale -->
36+
<column type="PyObject"/>
3537
</columns>
3638
</object>
3739
<object class="GtkListStore" id="progress_store">
@@ -173,6 +175,7 @@
173175
<child>
174176
<object class="GtkTreeViewColumn" id="intro_name_column">
175177
<property name="title">Qube name</property>
178+
<property name="expand">True</property>
176179
<property name="sort-column-id">3</property>
177180
<child>
178181
<object class="GtkCellRendererText" id="intro_name_renderer"/>
@@ -182,6 +185,7 @@
182185
<child>
183186
<object class="GtkTreeViewColumn" id="available_column">
184187
<property name="title">Updates available</property>
188+
<property name="expand">True</property>
185189
<property name="alignment">0.5</property>
186190
<property name="sort-column-id">4</property>
187191
<child>
@@ -192,6 +196,7 @@
192196
<child>
193197
<object class="GtkTreeViewColumn" id="check_column">
194198
<property name="title">Last checked</property>
199+
<property name="min-width">150</property>
195200
<property name="alignment">0.5</property>
196201
<property name="sort-column-id">5</property>
197202
<child>
@@ -202,6 +207,7 @@
202207
<child>
203208
<object class="GtkTreeViewColumn" id="update_column">
204209
<property name="title">Last updated</property>
210+
<property name="min-width">150</property>
205211
<property name="alignment">0.5</property>
206212
<property name="sort-column-id">6</property>
207213
<child>
@@ -230,7 +236,7 @@
230236
<property name="margin-bottom">20</property>
231237
<property name="label" translatable="yes">Qubes OS checks for updates for running and networked qubes and their templates. Updates may also be available in other qubes, marked as {MAYBE} above.
232238

233-
{OBSOLETE} qubes are based on templates that are no longer supported and no longer receive updates. Please install new templates using the Qubes Template Manager.
239+
{OBSOLETE} qubes are based on templates that are no longer supported and no longer receive updates. Please install new templates using the Qubes Template Manager. {PROHIBITED} qubes have the `prohibit-start` feature set.
234240

235241
Selected qubes will be automatically started if necessary and shutdown after successful update.</property>
236242
<property name="use-markup">True</property>

qui/updater/intro_page.py

+41-4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def __init__(self, builder, log, next_button):
6363
self.page: Gtk.Box = self.builder.get_object("list_page")
6464
self.stack: Gtk.Stack = self.builder.get_object("main_stack")
6565
self.vm_list: Gtk.TreeView = self.builder.get_object("vm_list")
66+
self.vm_list.set_has_tooltip(True)
67+
self.vm_list.connect("query-tooltip", self.on_query_tooltip)
6668
self.list_store: Optional[ListWrapper] = None
6769

6870
checkbox_column: Gtk.TreeViewColumn = self.builder.get_object(
@@ -93,6 +95,8 @@ def __init__(self, builder, log, next_button):
9395
"<b>MAYBE</b></span>",
9496
OBSOLETE=f'<span foreground="{label_color_theme("red")}">'
9597
"<b>OBSOLETE</b></span>",
98+
PROHIBITED=f'<span foreground="{label_color_theme("red")}">'
99+
"<b>START PROHIBITED</b></span>",
96100
)
97101
)
98102

@@ -307,6 +311,25 @@ def _handle_cli_dom0(dom0, to_update, cliargs):
307311
to_update = to_update.difference({"dom0"})
308312
return to_update
309313

314+
def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip):
315+
"""Show appropriate qube tooltip. Currently only for prohibit-start."""
316+
if not widget.get_tooltip_context(x, y, keyboard_tip):
317+
return False
318+
_, x, y, model, path, iterator = widget.get_tooltip_context(
319+
x, y, keyboard_tip
320+
)
321+
if path:
322+
status = model[iterator][4]
323+
if status == type(status).PROHIBITED:
324+
tooltip.set_text(
325+
"Start prohibition rationale:\n{}".format(
326+
str(model[iterator][9])
327+
)
328+
)
329+
widget.set_tooltip_cell(tooltip, path, None, None)
330+
return True
331+
return False
332+
310333

311334
def is_stale(vm, expiration_period):
312335
if expiration_period is None:
@@ -345,16 +368,20 @@ def __init__(self, list_store, vm, to_update: bool):
345368

346369
icon = load_icon(vm.icon)
347370
name = QubeName(vm.name, str(vm.label))
371+
prohibit_rationale = vm.features.get("prohibit-start", False)
348372

349373
raw_row = [
350374
selected,
351375
icon,
352376
name,
353-
UpdatesAvailable.from_features(updates_available, supported),
377+
UpdatesAvailable.from_features(
378+
updates_available, supported, bool(prohibit_rationale)
379+
),
354380
Date(last_updates_check),
355381
Date(last_update),
356382
0,
357383
UpdateStatus.Undefined,
384+
prohibit_rationale,
358385
]
359386

360387
super().__init__(list_store, vm, raw_row)
@@ -390,6 +417,7 @@ def updates_available(self):
390417

391418
@updates_available.setter
392419
def updates_available(self, value):
420+
prohibited = bool(self.vm.features.get("prohibit-start", False))
393421
updates_available = bool(
394422
self.vm.features.get("updates-available", False)
395423
)
@@ -398,7 +426,7 @@ def updates_available(self, value):
398426
if value and not updates_available:
399427
updates_available = None
400428
self.raw_row[self._UPDATES_AVAILABLE] = UpdatesAvailable.from_features(
401-
updates_available, supported
429+
updates_available, supported, prohibited
402430
)
403431

404432
@property
@@ -497,11 +525,16 @@ class UpdatesAvailable(Enum):
497525
MAYBE = 1
498526
NO = 2
499527
EOL = 3
528+
PROHIBITED = 4
500529

501530
@staticmethod
502531
def from_features(
503-
updates_available: Optional[bool], supported: Optional[str] = None
532+
updates_available: Optional[bool],
533+
supported: Optional[str] = None,
534+
prohibited: Optional[str] = None,
504535
) -> "UpdatesAvailable":
536+
if prohibited:
537+
return UpdatesAvailable.PROHIBITED
505538
if not supported:
506539
return UpdatesAvailable.EOL
507540
if updates_available:
@@ -512,6 +545,8 @@ def from_features(
512545

513546
@property
514547
def color(self):
548+
if self is UpdatesAvailable.PROHIBITED:
549+
return label_color_theme("red")
515550
if self is UpdatesAvailable.YES:
516551
return label_color_theme("green")
517552
if self is UpdatesAvailable.MAYBE:
@@ -522,7 +557,9 @@ def color(self):
522557
return label_color_theme("red")
523558

524559
def __str__(self):
525-
if self is UpdatesAvailable.YES:
560+
if self is UpdatesAvailable.PROHIBITED:
561+
name = "START PROHIBITED"
562+
elif self is UpdatesAvailable.YES:
526563
name = "YES"
527564
elif self is UpdatesAvailable.MAYBE:
528565
name = "MAYBE"

qui/updater/tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def test_qapp_impl():
208208
)
209209
add_feature_to_all(qapp, "os-eol", [])
210210
add_feature_to_all(qapp, "skip-update", [])
211+
add_feature_to_all(qapp, "prohibit-start", [])
211212

212213
return qapp
213214

qui/updater/tests/test_intro_page.py

+117
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,123 @@ def test_on_checkbox_toggled(
172172
assert not sut.checkbox_column_button.get_active()
173173

174174

175+
def test_prohibit_start(
176+
real_builder, test_qapp, mock_next_button, mock_settings, mock_list_store
177+
):
178+
mock_log = Mock()
179+
test_qapp.expected_calls[
180+
("test-standalone", "admin.vm.feature.Get", "prohibit-start", None)
181+
] = b"0\x00Control qube which should be un-selectable/un-updatable"
182+
sut = IntroPage(real_builder, mock_log, mock_next_button)
183+
184+
# populate_vm_list
185+
sut.list_store = ListWrapper(UpdateRowWrapper, mock_list_store)
186+
for vm in test_qapp.domains:
187+
sut.list_store.append_vm(vm)
188+
189+
assert len(sut.list_store) == 12
190+
191+
sut.head_checkbox.state = HeaderCheckbox.NONE
192+
sut.head_checkbox.set_buttons()
193+
194+
# If button is inconsistent we do not care if it is active or not
195+
# (we do not use this value)
196+
197+
# no selected row
198+
assert not sut.checkbox_column_button.get_inconsistent()
199+
assert not sut.checkbox_column_button.get_active()
200+
201+
# only one row selected
202+
sut.on_checkbox_toggled(_emitter=None, path=(3,))
203+
204+
assert sut.checkbox_column_button.get_inconsistent()
205+
206+
for i in range(len(sut.list_store)):
207+
sut.on_checkbox_toggled(_emitter=None, path=(i,))
208+
209+
# almost all rows selected (except one)
210+
assert sut.checkbox_column_button.get_inconsistent()
211+
212+
sut.on_checkbox_toggled(_emitter=None, path=(3,))
213+
214+
# almost all rows selected (except the start prohibited qube)
215+
assert sut.checkbox_column_button.get_inconsistent()
216+
assert not sut.checkbox_column_button.get_active()
217+
218+
sut.on_checkbox_toggled(_emitter=None, path=(3,))
219+
220+
# almost all rows selected (except one)
221+
assert sut.checkbox_column_button.get_inconsistent()
222+
223+
for i in range(len(sut.list_store)):
224+
if i == 3:
225+
continue
226+
sut.on_checkbox_toggled(_emitter=None, path=(i,))
227+
228+
# no selected row
229+
assert not sut.checkbox_column_button.get_inconsistent()
230+
assert not sut.checkbox_column_button.get_active()
231+
232+
# emulate mouse hover over vm_list header & area between rows
233+
side_effects = [False, True, (None, 1, 1, sut.list_store, False, None)]
234+
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
235+
sut.on_query_tooltip(sut.vm_list, 0, 0, None, None)
236+
sut.on_query_tooltip(sut.vm_list, 1, 1, None, None)
237+
238+
239+
def test_prohibit_start_rationale_tooltip(
240+
real_builder, test_qapp, mock_next_button, mock_settings, mock_list_store
241+
):
242+
mock_log = Mock()
243+
sut = IntroPage(real_builder, mock_log, mock_next_button)
244+
245+
# emulate mouse hover over an ordinary updateable qube
246+
side_effects = [
247+
True,
248+
[
249+
None,
250+
2,
251+
2,
252+
[[0, 1, 2, 3, UpdatesAvailable.YES, 5, 6, 7, 8, ""]],
253+
True,
254+
0,
255+
],
256+
]
257+
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
258+
sut.on_query_tooltip(sut.vm_list, 2, 2, None, None)
259+
260+
# emulate mouse hover over an start prohibited qube
261+
side_effects = [
262+
True,
263+
[
264+
None,
265+
2,
266+
2,
267+
[
268+
[
269+
0,
270+
1,
271+
2,
272+
3,
273+
UpdatesAvailable.PROHIBITED,
274+
5,
275+
6,
276+
7,
277+
8,
278+
"DO NOT UPDATE",
279+
],
280+
],
281+
True,
282+
0,
283+
],
284+
]
285+
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
286+
sut.vm_list.set_tooltip_cell = Mock()
287+
mock_tooltip = Mock()
288+
sut.on_query_tooltip(sut.vm_list, 3, 3, None, mock_tooltip)
289+
assert sut.vm_list.set_tooltip_cell.called
290+
291+
175292
doms = test_qapp_impl().domains
176293
_domains = {vm.name for vm in doms}
177294
_templates = {vm.name for vm in doms if vm.klass == "TemplateVM"}

qui/updater/updater.py

+1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ def cell_data_func(_column, cell, model, it, data):
232232
int(width * 1.2), self.main_window.get_screen().get_height() - 48
233233
)
234234
self.main_window.resize(width + 50, height)
235+
self.main_window.set_size_request(800, 600) # Smaller is meaningless
235236
self.main_window.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
236237

237238
def open_settings_window(self, _emitter):

qui/updater/utils.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,11 @@ def append_vm(self, vm, state: bool = False):
307307

308308
def invert_selection(self, path):
309309
it = self.list_store_raw.get_iter(path)
310-
self.list_store_raw[it][0].selected = not self.list_store_raw[it][
311-
0
312-
].selected
310+
UpdatesAvailable = self.list_store_raw[it][4]
311+
if "PROHIBITED" not in str(UpdatesAvailable):
312+
self.list_store_raw[it][0].selected = not self.list_store_raw[it][
313+
0
314+
].selected
313315

314316
def get_selected(self) -> "ListWrapper":
315317
empty_copy = Gtk.ListStore(

0 commit comments

Comments
 (0)