diff --git a/qubes_config/global_config.glade b/qubes_config/global_config.glade index 10c5d18f..19bba8e2 100644 --- a/qubes_config/global_config.glade +++ b/qubes_config/global_config.glade @@ -976,7 +976,7 @@ False natural - + True False @@ -1069,7 +1069,7 @@ 0 - 11 + 12 2 @@ -1085,7 +1085,7 @@ 0 - 14 + 15 @@ -1101,7 +1101,7 @@ 1 - 14 + 15 @@ -1114,7 +1114,7 @@ 1 - 15 + 16 @@ -1127,7 +1127,7 @@ 1 - 16 + 17 @@ -1175,7 +1175,7 @@ 1 - 6 + 7 @@ -1199,7 +1199,7 @@ 1 - 7 + 8 @@ -1223,7 +1223,7 @@ 1 - 5 + 6 @@ -1238,7 +1238,7 @@ 0 - 18 + 19 2 @@ -1255,7 +1255,7 @@ 0 - 19 + 20 2 @@ -1271,7 +1271,7 @@ 0 - 23 + 24 2 @@ -1283,7 +1283,7 @@ 1 - 25 + 26 @@ -1295,7 +1295,7 @@ 0 - 26 + 27 2 @@ -1312,7 +1312,7 @@ 0 - 24 + 25 2 @@ -1357,7 +1357,7 @@ 0 - 5 + 6 @@ -1401,7 +1401,7 @@ 0 - 6 + 7 @@ -1446,7 +1446,7 @@ 0 - 7 + 8 @@ -1462,7 +1462,7 @@ 0 - 12 + 13 2 @@ -1509,7 +1509,7 @@ 0 - 13 + 14 2 @@ -1554,7 +1554,7 @@ 0 - 15 + 16 @@ -1578,7 +1578,7 @@ 0 - 10 + 11 @@ -1590,7 +1590,7 @@ 0 - 17 + 18 @@ -1602,7 +1602,7 @@ 0 - 22 + 23 @@ -1633,7 +1633,7 @@ 0 - 16 + 17 @@ -1648,7 +1648,7 @@ 0 - 25 + 26 @@ -1663,7 +1663,7 @@ 0 - 20 + 21 @@ -1678,7 +1678,7 @@ 0 - 21 + 22 @@ -1718,7 +1718,7 @@ 1 - 20 + 21 @@ -1758,7 +1758,7 @@ 1 - 21 + 22 @@ -1815,7 +1815,7 @@ 0 - 8 + 9 @@ -1882,7 +1882,7 @@ 1 - 8 + 9 @@ -1925,7 +1925,7 @@ 1 - 9 + 10 @@ -1953,7 +1953,33 @@ 0 - 9 + 10 + + + + + False + vertical + + + True + False + True + + + False + True + 0 + + + + + + 0 + 5 + 2 diff --git a/qubes_config/global_config/basics_handler.py b/qubes_config/global_config/basics_handler.py index a8f906ea..4c8b828f 100644 --- a/qubes_config/global_config/basics_handler.py +++ b/qubes_config/global_config/basics_handler.py @@ -43,6 +43,7 @@ get_feature, get_boolean_feature, apply_feature_change, + inadvisable_selection, ) import gi @@ -130,9 +131,11 @@ def __init__( self, qapp: qubesadmin.Qubes, trait_holder: Any, - trait_name: str, widget: Gtk.ComboBox, + trait_name: str, + gtk_builder: Gtk.Builder = None, vm_filter: Optional[Callable] = None, + vm_inadvisable: Optional[Callable] = None, readable_name: Optional[str] = None, additional_options: Dict[Any | str | None, str] | None = None, show_internal: bool = False, @@ -146,7 +149,10 @@ def __init__( self.model = VMListModeler( combobox=self.widget, qapp=qapp, + gtk_builder=gtk_builder, + trait_name=trait_name, filter_function=vm_filter, + vm_inadvisable=vm_inadvisable, current_value=self.get_current_value(), style_changes=True, additional_options=additional_options, @@ -644,7 +650,9 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): trait_holder=self.qapp, trait_name="clockvm", widget=self.clockvm_combo, + gtk_builder=gtk_builder, vm_filter=self._clock_vm_filter, + vm_inadvisable=self._clock_vm_inadvisable, readable_name=_("Clock qube"), additional_options=NONE_CATEGORY, ) @@ -742,7 +750,15 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): @staticmethod def _clock_vm_filter(vm) -> bool: - return vm.klass != "TemplateVM" + return vm.klass != "TemplateVM" and (vm.klass == "AdminVM" or vm.is_networked()) + + @staticmethod + def _clock_vm_inadvisable(vm) -> str: + return ( + inadvisable_selection( + readable_name="clock qube", vm=vm, warn_dispvm_template=True + ), + ) @staticmethod def _default_template_filter(vm) -> bool: diff --git a/qubes_config/global_config/device_attachments.py b/qubes_config/global_config/device_attachments.py index 0299821f..4974beb1 100644 --- a/qubes_config/global_config/device_attachments.py +++ b/qubes_config/global_config/device_attachments.py @@ -31,6 +31,7 @@ ProtocolError, ) +from ..widgets.utils import inadvisable_selection from ..widgets.gtk_widgets import TokenName from ..widgets.gtk_utils import show_error from .device_widgets import ( @@ -122,7 +123,12 @@ def __init__( self.qapp, "edit_device", [], - filter_function=lambda vm: vm.klass != "AdminVM", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"], + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="qube for having devices", + vm=vm, + warn_dispvm_template=True, + ), ) self.backend_vm: Optional[qubesadmin.vm.QubesVM] = None @@ -398,7 +404,12 @@ def __init__( self.qapp, "required_device", [], - filter_function=lambda vm: vm.klass != "AdminVM", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"], + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="qube for required device", + vm=vm, + warn_dispvm_template=True, + ), ) self.dev_modeler = self.fill_combo_with_devices( diff --git a/qubes_config/global_config/global_config.py b/qubes_config/global_config/global_config.py index 8bb9292e..4df65dec 100644 --- a/qubes_config/global_config/global_config.py +++ b/qubes_config/global_config/global_config.py @@ -40,7 +40,7 @@ show_dialog, ) from ..widgets.gtk_widgets import ProgressBarDialog, ViewportHandler -from ..widgets.utils import open_url_in_disposable +from ..widgets.utils import open_url_in_disposable, inadvisable_selection from .page_handler import PageHandler from .policy_handler import PolicyHandler, VMSubsetPolicyHandler from .policy_rules import ( @@ -434,6 +434,13 @@ def perform_setup(self): service_name="qubes.Gpg", policy_file_name="50-config-splitgpg", default_policy="", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] + and not vm.features.get("service.guivm") + and not vm.features.get("service.audiovm") + and not getattr(vm, "provides_network", False), + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="key qube", vm=vm, warn_dispvm_template=True + ), main_rule_class=RuleSimpleNoAllow, main_verb_description=SimpleVerbDescription( { diff --git a/qubes_config/global_config/policy_handler.py b/qubes_config/global_config/policy_handler.py index 57ecce76..0fb63b97 100644 --- a/qubes_config/global_config/policy_handler.py +++ b/qubes_config/global_config/policy_handler.py @@ -44,7 +44,7 @@ import qubesadmin import qubesadmin.vm -from ..widgets.utils import compare_rule_lists +from ..widgets.utils import compare_rule_lists, get_boolean_feature gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -562,6 +562,8 @@ def __init__( main_rule_class: Type[AbstractRuleWrapper], exception_verb_description: AbstractVerbDescription, exception_rule_class: Type[AbstractRuleWrapper], + filter_function: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, + vm_inadvisable: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, ): """ :param qapp: Qubes object @@ -581,6 +583,7 @@ def __init__( the exception rules :param exception_rule_class: class to be used for exception Rules, must inherit from AbstractRuleWrapper + :param vm_inadvisable: warn if selection is not advisable """ self.select_qubes: Set[str] = set() self.main_verb_description = main_verb_description @@ -619,7 +622,25 @@ def __init__( # populate combo self.select_qube_model = VMListModeler( - combobox=self.select_qube_combo, qapp=self.qapp + combobox=self.select_qube_combo, + trait_name=prefix, + gtk_builder=gtk_builder, + qapp=self.qapp, + filter_function=( + ( + lambda vm: filter_function(vm) + and get_boolean_feature( + vm, "supported-rpc." + service_name, recurse_template=True + ) + ) + if filter_function + else ( + lambda vm: get_boolean_feature( + vm, "supported-rpc." + service_name, recurse_template=True + ) + ) + ), + vm_inadvisable=vm_inadvisable, ) # connect events diff --git a/qubes_config/global_config/updates_handler.py b/qubes_config/global_config/updates_handler.py index a7dfd00d..27765ff6 100644 --- a/qubes_config/global_config/updates_handler.py +++ b/qubes_config/global_config/updates_handler.py @@ -28,7 +28,11 @@ from qrexec.client import call as qrexec_call from ..widgets.gtk_widgets import VMListModeler, NONE_CATEGORY -from ..widgets.utils import get_boolean_feature, apply_feature_change +from ..widgets.utils import ( + get_boolean_feature, + apply_feature_change, + inadvisable_selection, +) from .page_handler import PageHandler from .policy_rules import RuleTargeted, SimpleVerbDescription from .policy_handler import PolicyHandler @@ -681,11 +685,19 @@ def __init__( self.dom0_updatevm_model = VMListModeler( combobox=self.dom0_updatevm_combo, + trait_name="dom0_updatevm", + gtk_builder=gtk_builder, qapp=self.qapp, filter_function=( lambda vm: vm.klass != "TemplateVM" and vm.klass != "AdminVM" and vm.is_networked() + and get_boolean_feature( + vm, "supported-rpc.qubes.TemplateDownload", recurse_template=True + ) + ), + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="dom0 updates proxy", vm=vm, warn_dispvm_template=True ), current_value=self.qapp.updatevm, additional_options=NONE_CATEGORY, diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index 096613e8..f8910b5f 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -27,7 +27,12 @@ from qubesadmin.device_protocol import DeviceCategory from ..widgets.gtk_widgets import TokenName, TextModeler, VMListModeler -from ..widgets.utils import get_feature, apply_feature_change +from ..widgets.utils import ( + get_feature, + get_boolean_feature, + apply_feature_change, + dispvm_template_inadvisable, +) from ..widgets.gtk_utils import ask_question, show_error from .page_handler import PageHandler from .policy_rules import RuleTargetedAdminVM, Rule @@ -317,6 +322,7 @@ def __init__( "usb_u2f_enable_some", self.initially_enabled_vms, lambda vm: vm in self.available_vms, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.register_some_handler = VMFlowboxHandler( @@ -326,6 +332,7 @@ def __init__( self.initial_register_vms, lambda vm: vm in self.available_vms, verification_callback=self._verify_additional_vm, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.blanket_handler = VMFlowboxHandler( @@ -335,6 +342,7 @@ def __init__( self.initial_blanket_vms, lambda vm: vm in self.available_vms, verification_callback=self._verify_additional_vm, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.widget_to_box = { @@ -465,13 +473,16 @@ def load_rules_for_usb_qube(self): self.error_handler.clear_all_errors() for vm in self.qapp.domains: - if vm.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE): - if vm == usb_qube: - continue + if vm == usb_qube: + continue + if vm.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE) and not ( + vm.klass == "TemplateVM" + or getattr(vm, "provides_network", False) + or get_boolean_feature(vm, "service.audiovm") + or get_boolean_feature(vm, "service.guivm") + ): self.available_vms.append(vm) if get_feature(vm, self.SERVICE_FEATURE): - if vm == usb_qube: - continue self.initially_enabled_vms.append(vm) if not self.available_vms: diff --git a/qubes_config/global_config/vm_flowbox.py b/qubes_config/global_config/vm_flowbox.py index 03e88fd2..f20660bd 100644 --- a/qubes_config/global_config/vm_flowbox.py +++ b/qubes_config/global_config/vm_flowbox.py @@ -109,6 +109,7 @@ def __init__( prefix: str, initial_vms: List[qubesadmin.vm.QubesVM], filter_function: Optional[Callable] = None, + vm_inadvisable: Optional[Callable] = None, verification_callback: Optional[Callable[[qubesadmin.vm.QubesVM], bool]] = None, ): """ @@ -117,6 +118,7 @@ def __init__( :param prefix: widget name prefix (see above) :param initial_vms: list of initially selected vms :param filter_function: function to filter vms available in the dropdown + :param vm_inadvisable: function to warn if selection is inadvisable :param verification_callback: if provided, will be called before adding a vm; return True if verification was successful and false if it has failed @@ -134,8 +136,11 @@ def __init__( self.add_qube_model = VMListModeler( combobox=self.qube_combo, + trait_name=prefix, + gtk_builder=gtk_builder, qapp=self.qapp, filter_function=filter_function, + vm_inadvisable=vm_inadvisable, ) self.add_qube_model.connect_change_callback(self._check_for_add_validity) diff --git a/qubes_config/qubes-global-config-base.css b/qubes_config/qubes-global-config-base.css index 0b2a75e4..639954c2 100644 --- a/qubes_config/qubes-global-config-base.css +++ b/qubes_config/qubes-global-config-base.css @@ -187,6 +187,16 @@ separator { background: @problem-background; } +.problem_box_to_previous { + margin: 0px 0px 10px 0px; + padding: 10px; + border-width: 2px; + border-style: solid; + border-radius: 4px; + border-color: @problem-frame; + background: @problem-background; +} + .problem_list { margin-top: 10px; background: @problem-background; @@ -282,4 +292,4 @@ separator { color: @red-label; font-style: italic; font-weight: bold; -} \ No newline at end of file +} diff --git a/qubes_config/tests/test_policy_handler.py b/qubes_config/tests/test_policy_handler.py index 2414d677..2c28a9be 100644 --- a/qubes_config/tests/test_policy_handler.py +++ b/qubes_config/tests/test_policy_handler.py @@ -886,6 +886,11 @@ def test_subset_handler(test_builder, test_qapp, test_policy_manager: PolicyMana default_policy = """ TestService * @anyvm test-blue allow""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -973,6 +978,11 @@ def test_subset_handler_limited_choice( TestService * @anyvm test-blue allow TestService * @anyvm test-red allow""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1066,6 +1076,11 @@ def test_subset_handler_remove_choice( TestService * @anyvm test-blue allow TestService * @anyvm vault ask""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1122,6 +1137,11 @@ def test_subset_handler_duplicates( default_policy = """ TestService * @anyvm vault ask""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1161,6 +1181,11 @@ def test_subset_handler_duplicates_load( TestService * @anyvm vault ask """ + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1191,6 +1216,11 @@ def test_subset_handler_unsupported( TestService * @anyvm vault allow target=test-blue """ + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = VMSubsetPolicyHandler( diff --git a/qubes_config/tests/test_usb_devices.py b/qubes_config/tests/test_usb_devices.py index 40087c02..9d8f53c0 100644 --- a/qubes_config/tests/test_usb_devices.py +++ b/qubes_config/tests/test_usb_devices.py @@ -350,7 +350,7 @@ def test_u2f_handler_init(test_qapp, test_policy_manager, real_builder): assert handler.enable_check.get_active() assert handler.enable_some_handler.selected_vms == [testvm] assert handler.enable_some_handler.add_qube_model.is_vm_available(testvm) - assert handler.enable_some_handler.add_qube_model.is_vm_available(fedora35) + assert not handler.enable_some_handler.add_qube_model.is_vm_available(fedora35) assert not handler.enable_some_handler.add_qube_model.is_vm_available(testred) assert not handler.enable_some_handler.add_qube_model.is_vm_available(sysusb) @@ -856,9 +856,9 @@ def test_u2f_handler_save_complex(test_qapp, test_policy_manager, real_builder): ) assert ( call(test_qapp.domains["fedora-35"], handler.SERVICE_FEATURE, True) - in mock_apply.mock_calls + not in mock_apply.mock_calls ) - assert len(mock_apply.mock_calls) == 3 + assert len(mock_apply.mock_calls) == 2 expected_rules = handler.policy_manager.text_to_rules( """ @@ -915,9 +915,9 @@ def test_u2f_handler_save_complex_2(test_qapp, test_policy_manager, real_builder ) assert ( call(test_qapp.domains["fedora-35"], handler.SERVICE_FEATURE, True) - in mock_apply.mock_calls + not in mock_apply.mock_calls ) - assert len(mock_apply.mock_calls) == 3 + assert len(mock_apply.mock_calls) == 2 expected_rules = handler.policy_manager.text_to_rules( """ @@ -933,14 +933,14 @@ def test_u2f_handler_save_complex_2(test_qapp, test_policy_manager, real_builder def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - fedora35 = test_qapp.domains["fedora-35"] + testblue = test_qapp.domains["test-blue"] testvm = test_qapp.domains["test-vm"] handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.get_unsaved() == "" # settings from conftest: only vms that have this available are 'test-vm' - # and 'fedora-35', only test-vm can use the service, policy is default + # and 'test-blue', only test-vm can use the service, policy is default handler.register_check.set_active(True) handler.register_some_radio.set_active(True) @@ -948,7 +948,7 @@ def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_bu assert not handler.register_some_handler.selected_vms assert handler.enable_some_handler.selected_vms == [testvm] - handler.register_some_handler.add_qube_model.select_value("fedora-35") + handler.register_some_handler.add_qube_model.select_value("test-blue") # refuse with patch("qubes_config.global_config.usb_devices.ask_question") as mock_question: mock_question.return_value = Gtk.ResponseType.NO @@ -962,9 +962,9 @@ def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_bu mock_question.return_value = Gtk.ResponseType.YES handler.register_some_handler.add_button.clicked() assert mock_question.mock_calls - assert handler.register_some_handler.selected_vms == [fedora35] + assert handler.register_some_handler.selected_vms == [testblue] - assert handler.enable_some_handler.selected_vms == [fedora35, testvm] + assert handler.enable_some_handler.selected_vms == [testblue, testvm] def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): @@ -981,6 +981,12 @@ def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " b"_device='0d'\n" ) + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.audiovm", None) + ] = b"0\x00" + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.guivm", None) + ] = b"0\x00" handler = DevicesHandler(test_qapp, test_policy_manager, real_builder) @@ -1023,6 +1029,12 @@ def test_devices_handler_detect_usbvms(test_qapp, test_policy_manager, real_buil b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " b"_device='0d'\n" ) + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.audiovm", None) + ] = b"0\x00" + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.guivm", None) + ] = b"0\x00" handler = DevicesHandler(test_qapp, test_policy_manager, real_builder) diff --git a/qubes_config/widgets/gtk_widgets.py b/qubes_config/widgets/gtk_widgets.py index 68689487..ab365371 100644 --- a/qubes_config/widgets/gtk_widgets.py +++ b/qubes_config/widgets/gtk_widgets.py @@ -230,7 +230,10 @@ def __init__( self, combobox: Gtk.ComboBox, qapp: qubesadmin.Qubes, + trait_name: str = "", + gtk_builder: Gtk.Builder = None, filter_function: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, + vm_inadvisable: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, event_callback: Callable[[], None] | None = None, default_value: qubesadmin.vm.QubesVM | str | None = None, current_value: qubesadmin.vm.QubesVM | str | None = None, @@ -241,9 +244,13 @@ def __init__( """ :param combobox: target ComboBox object :param qapp: Qubes object, necessary to retrieve VM info + :param trait_name: trait that is being considered + :param gtk_builder: builder passed from upstream :param filter_function: function used to filter VMs, must take as input QubesVM object and return bool; caution: remember not all properties are always available for all VMs, in particular dom0 can cause problems + :param vm_inadvisable: function used to return human-readable information if + selected qube is not recommended. :param event_callback: function to be called whenever combobox value changes :param default_value: default VM (will get a (default) decoration @@ -262,6 +269,7 @@ def __init__( self.qapp = qapp self.combo = combobox self.entry_box = self.combo.get_child() + self.vm_inadvisable = vm_inadvisable self.change_function = event_callback self.style_changes = style_changes self.show_internal = show_internal @@ -279,6 +287,15 @@ def __init__( self._initial_id = None + if gtk_builder: + self.warn_box = gtk_builder.get_object(trait_name + "_warn_box") + self.warn_label: Gtk.Label = gtk_builder.get_object( + trait_name + "_warn_label" + ) + self.warn_box.set_visible(False) + else: + self.warn_box = None + if current_value: self.select_value(current_value) elif default_value: @@ -397,17 +414,28 @@ def _get_valid_qube_name(self): def _combo_change(self, _widget): name = self._get_valid_qube_name() + inadvisable = "" if name: entry = self._entries[name] self.entry_box.set_icon_from_pixbuf( Gtk.EntryIconPosition.PRIMARY, entry["icon"] ) + vm = entry["vm"] + if vm and self.vm_inadvisable and (inadvisable := self.vm_inadvisable(vm)): + self._warn(inadvisable) else: self.entry_box.set_icon_from_pixbuf( Gtk.EntryIconPosition.PRIMARY, load_icon("gtk-find", 18, 18) ) + if ( + self.warn_box is not None + and not inadvisable + and self.warn_box.get_visible() + ): + self.warn_box.set_visible(False) + if self.change_function: self.change_function() @@ -500,6 +528,11 @@ def select_value(self, vm_name): if entry["api_name"] == vm_name: self.combo.set_active_id(display_name) + def _warn(self, error_descr: str): + self.warn_label.set_text(error_descr) + if self.warn_box is not None: + self.warn_box.set_visible(True) + def is_vm_available(self, vm: qubesadmin.vm.QubesVM) -> bool: """Check if given VM is available in the list.""" for entry in self._entries.values(): diff --git a/qubes_config/widgets/utils.py b/qubes_config/widgets/utils.py index f413390e..1a0b8123 100644 --- a/qubes_config/widgets/utils.py +++ b/qubes_config/widgets/utils.py @@ -33,19 +33,25 @@ _ = t.gettext -def get_feature(vm, feature_name, default_value=None): +def get_feature(vm, feature_name, default_value=None, recurse_template=False): """Get feature, with a working default_value.""" + if recurse_template: + feat = vm.features.check_with_template + else: + feat = vm.features.get try: - return vm.features.get(feature_name, default_value) + return feat(feature_name, default_value) except qubesadmin.exc.QubesDaemonAccessError: return default_value -def get_boolean_feature(vm, feature_name, default=False): +def get_boolean_feature(vm, feature_name, default=False, recurse_template=False): """helper function to get a feature converted to a Bool if it does exist. Necessary because of the true/false in features being coded as 1/empty string.""" - result = get_feature(vm, feature_name, None) + result = get_feature( + vm, feature_name, default_value=None, recurse_template=recurse_template + ) if result is not None: result = bool(result) else: @@ -141,3 +147,21 @@ def open_url_in_disposable(url: str, qapp: qubesadmin.Qubes): group=None, target=_open_url_in_dvm, args=[url, default_dvm] ) open_thread.start() + + +def inadvisable_selection( + readable_name: str, vm: qubesadmin.vm.QubesVM, warn_dispvm_template: bool = False +) -> str: + inadvisable = [] + if warn_dispvm_template: + inadvisable.append(dispvm_template_inadvisable(vm)) + pretty_inadvisable = " ".join(i for i in inadvisable if i) + if not pretty_inadvisable: + return "" + return f"Inadvisable {readable_name.lower()}: {pretty_inadvisable}" + + +def dispvm_template_inadvisable(vm: qubesadmin.vm.QubesVM) -> str: + if getattr(vm, "template_for_dispvms", False): + return "It's a disposable template, normally not intended for such cases." + return ""