Skip to content
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
8 changes: 7 additions & 1 deletion qui/updater.glade
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
<column type="gint"/>
<!-- column-name gchararray1 -->
<column type="PyObject"/>
<!-- column-name rationale -->
<column type="PyObject"/>
</columns>
</object>
<object class="GtkListStore" id="progress_store">
Expand Down Expand Up @@ -173,6 +175,7 @@
<child>
<object class="GtkTreeViewColumn" id="intro_name_column">
<property name="title">Qube name</property>
<property name="expand">True</property>
<property name="sort-column-id">3</property>
<child>
<object class="GtkCellRendererText" id="intro_name_renderer"/>
Expand All @@ -182,6 +185,7 @@
<child>
<object class="GtkTreeViewColumn" id="available_column">
<property name="title">Updates available</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort-column-id">4</property>
<child>
Expand All @@ -192,6 +196,7 @@
<child>
<object class="GtkTreeViewColumn" id="check_column">
<property name="title">Last checked</property>
<property name="min-width">150</property>
<property name="alignment">0.5</property>
<property name="sort-column-id">5</property>
<child>
Expand All @@ -202,6 +207,7 @@
<child>
<object class="GtkTreeViewColumn" id="update_column">
<property name="title">Last updated</property>
<property name="min-width">150</property>
<property name="alignment">0.5</property>
<property name="sort-column-id">6</property>
<child>
Expand Down Expand Up @@ -230,7 +236,7 @@
<property name="margin-bottom">20</property>
<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.

{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.
{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.

Selected qubes will be automatically started if necessary and shutdown after successful update.</property>
<property name="use-markup">True</property>
Expand Down
45 changes: 41 additions & 4 deletions qui/updater/intro_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def __init__(self, builder, log, next_button):
self.page: Gtk.Box = self.builder.get_object("list_page")
self.stack: Gtk.Stack = self.builder.get_object("main_stack")
self.vm_list: Gtk.TreeView = self.builder.get_object("vm_list")
self.vm_list.set_has_tooltip(True)
self.vm_list.connect("query-tooltip", self.on_query_tooltip)
self.list_store: Optional[ListWrapper] = None

checkbox_column: Gtk.TreeViewColumn = self.builder.get_object(
Expand Down Expand Up @@ -93,6 +95,8 @@ def __init__(self, builder, log, next_button):
"<b>MAYBE</b></span>",
OBSOLETE=f'<span foreground="{label_color_theme("red")}">'
"<b>OBSOLETE</b></span>",
PROHIBITED=f'<span foreground="{label_color_theme("red")}">'
"<b>START PROHIBITED</b></span>",
)
)

Expand Down Expand Up @@ -307,6 +311,25 @@ def _handle_cli_dom0(dom0, to_update, cliargs):
to_update = to_update.difference({"dom0"})
return to_update

def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip):
"""Show appropriate qube tooltip. Currently only for prohibit-start."""
if not widget.get_tooltip_context(x, y, keyboard_tip):
return False
_, x, y, model, path, iterator = widget.get_tooltip_context(
x, y, keyboard_tip
)
if path:
status = model[iterator][4]
if status == type(status).PROHIBITED:
tooltip.set_text(
"Start prohibition rationale:\n{}".format(
str(model[iterator][9])
)
)
widget.set_tooltip_cell(tooltip, path, None, None)
return True
return False


def is_stale(vm, expiration_period):
if expiration_period is None:
Expand Down Expand Up @@ -345,16 +368,20 @@ def __init__(self, list_store, vm, to_update: bool):

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

raw_row = [
selected,
icon,
name,
UpdatesAvailable.from_features(updates_available, supported),
UpdatesAvailable.from_features(
updates_available, supported, bool(prohibit_rationale)
),
Date(last_updates_check),
Date(last_update),
0,
UpdateStatus.Undefined,
prohibit_rationale,
]

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

@updates_available.setter
def updates_available(self, value):
prohibited = bool(self.vm.features.get("prohibit-start", False))
updates_available = bool(
self.vm.features.get("updates-available", False)
)
Expand All @@ -398,7 +426,7 @@ def updates_available(self, value):
if value and not updates_available:
updates_available = None
self.raw_row[self._UPDATES_AVAILABLE] = UpdatesAvailable.from_features(
updates_available, supported
updates_available, supported, prohibited
)

@property
Expand Down Expand Up @@ -497,11 +525,16 @@ class UpdatesAvailable(Enum):
MAYBE = 1
NO = 2
EOL = 3
PROHIBITED = 4

@staticmethod
def from_features(
updates_available: Optional[bool], supported: Optional[str] = None
updates_available: Optional[bool],
supported: Optional[str] = None,
prohibited: Optional[str] = None,
) -> "UpdatesAvailable":
if prohibited:
return UpdatesAvailable.PROHIBITED
if not supported:
return UpdatesAvailable.EOL
if updates_available:
Expand All @@ -512,6 +545,8 @@ def from_features(

@property
def color(self):
if self is UpdatesAvailable.PROHIBITED:
return label_color_theme("red")
if self is UpdatesAvailable.YES:
return label_color_theme("green")
if self is UpdatesAvailable.MAYBE:
Expand All @@ -522,7 +557,9 @@ def color(self):
return label_color_theme("red")

def __str__(self):
if self is UpdatesAvailable.YES:
if self is UpdatesAvailable.PROHIBITED:
name = "START PROHIBITED"
elif self is UpdatesAvailable.YES:
name = "YES"
elif self is UpdatesAvailable.MAYBE:
name = "MAYBE"
Expand Down
1 change: 1 addition & 0 deletions qui/updater/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def test_qapp_impl():
)
add_feature_to_all(qapp, "os-eol", [])
add_feature_to_all(qapp, "skip-update", [])
add_feature_to_all(qapp, "prohibit-start", [])

return qapp

Expand Down
117 changes: 117 additions & 0 deletions qui/updater/tests/test_intro_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,123 @@ def test_on_checkbox_toggled(
assert not sut.checkbox_column_button.get_active()


def test_prohibit_start(
real_builder, test_qapp, mock_next_button, mock_settings, mock_list_store
):
mock_log = Mock()
test_qapp.expected_calls[
("test-standalone", "admin.vm.feature.Get", "prohibit-start", None)
] = b"0\x00Control qube which should be un-selectable/un-updatable"
sut = IntroPage(real_builder, mock_log, mock_next_button)

# populate_vm_list
sut.list_store = ListWrapper(UpdateRowWrapper, mock_list_store)
for vm in test_qapp.domains:
sut.list_store.append_vm(vm)

assert len(sut.list_store) == 12

sut.head_checkbox.state = HeaderCheckbox.NONE
sut.head_checkbox.set_buttons()

# If button is inconsistent we do not care if it is active or not
# (we do not use this value)

# no selected row
assert not sut.checkbox_column_button.get_inconsistent()
assert not sut.checkbox_column_button.get_active()

# only one row selected
sut.on_checkbox_toggled(_emitter=None, path=(3,))

assert sut.checkbox_column_button.get_inconsistent()

for i in range(len(sut.list_store)):
sut.on_checkbox_toggled(_emitter=None, path=(i,))

# almost all rows selected (except one)
assert sut.checkbox_column_button.get_inconsistent()

sut.on_checkbox_toggled(_emitter=None, path=(3,))

# almost all rows selected (except the start prohibited qube)
assert sut.checkbox_column_button.get_inconsistent()
assert not sut.checkbox_column_button.get_active()

sut.on_checkbox_toggled(_emitter=None, path=(3,))

# almost all rows selected (except one)
assert sut.checkbox_column_button.get_inconsistent()

for i in range(len(sut.list_store)):
if i == 3:
continue
sut.on_checkbox_toggled(_emitter=None, path=(i,))

# no selected row
assert not sut.checkbox_column_button.get_inconsistent()
assert not sut.checkbox_column_button.get_active()

# emulate mouse hover over vm_list header & area between rows
side_effects = [False, True, (None, 1, 1, sut.list_store, False, None)]
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
sut.on_query_tooltip(sut.vm_list, 0, 0, None, None)
sut.on_query_tooltip(sut.vm_list, 1, 1, None, None)


def test_prohibit_start_rationale_tooltip(
real_builder, test_qapp, mock_next_button, mock_settings, mock_list_store
):
mock_log = Mock()
sut = IntroPage(real_builder, mock_log, mock_next_button)

# emulate mouse hover over an ordinary updateable qube
side_effects = [
True,
[
None,
2,
2,
[[0, 1, 2, 3, UpdatesAvailable.YES, 5, 6, 7, 8, ""]],
True,
0,
],
]
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
sut.on_query_tooltip(sut.vm_list, 2, 2, None, None)

# emulate mouse hover over an start prohibited qube
side_effects = [
True,
[
None,
2,
2,
[
[
0,
1,
2,
3,
UpdatesAvailable.PROHIBITED,
5,
6,
7,
8,
"DO NOT UPDATE",
],
],
True,
0,
],
]
sut.vm_list.get_tooltip_context = Mock(side_effect=side_effects)
sut.vm_list.set_tooltip_cell = Mock()
mock_tooltip = Mock()
sut.on_query_tooltip(sut.vm_list, 3, 3, None, mock_tooltip)
assert sut.vm_list.set_tooltip_cell.called


doms = test_qapp_impl().domains
_domains = {vm.name for vm in doms}
_templates = {vm.name for vm in doms if vm.klass == "TemplateVM"}
Expand Down
1 change: 1 addition & 0 deletions qui/updater/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def cell_data_func(_column, cell, model, it, data):
int(width * 1.2), self.main_window.get_screen().get_height() - 48
)
self.main_window.resize(width + 50, height)
self.main_window.set_size_request(800, 600) # Smaller is meaningless
self.main_window.set_position(Gtk.WindowPosition.CENTER_ALWAYS)

def open_settings_window(self, _emitter):
Expand Down
8 changes: 5 additions & 3 deletions qui/updater/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,11 @@ def append_vm(self, vm, state: bool = False):

def invert_selection(self, path):
it = self.list_store_raw.get_iter(path)
self.list_store_raw[it][0].selected = not self.list_store_raw[it][
0
].selected
UpdatesAvailable = self.list_store_raw[it][4]
if "PROHIBITED" not in str(UpdatesAvailable):
self.list_store_raw[it][0].selected = not self.list_store_raw[it][
0
].selected

def get_selected(self) -> "ListWrapper":
empty_copy = Gtk.ListStore(
Expand Down