From c3cb11b892823b406bebc4f0e4982e73465b43d4 Mon Sep 17 00:00:00 2001 From: Martin Kolman Date: Tue, 5 Nov 2024 17:04:11 +0100 Subject: [PATCH] Subscription code updates - GTK GUI Adjust the GTK GUI for the Subscription code changes: - reflect Satellite being used in the GUI - better UX when Simple Content Access (SCA) is used - drop support for displaying attached entitlements (support for entitlements has been dropped in RHEL 10 system-wide) Resolves: RHEL-49661 Related: INSTALLER-3903 Related: INSTALLER-3903 --- .../ui/gui/spokes/installation_source.py | 45 +- pyanaconda/ui/gui/spokes/lib/subscription.py | 131 ---- pyanaconda/ui/gui/spokes/subscription.glade | 601 +++++++++++------- pyanaconda/ui/gui/spokes/subscription.py | 116 +++- 4 files changed, 498 insertions(+), 395 deletions(-) diff --git a/pyanaconda/ui/gui/spokes/installation_source.py b/pyanaconda/ui/gui/spokes/installation_source.py index 0c98e17ff27..3c5a76ba2f2 100644 --- a/pyanaconda/ui/gui/spokes/installation_source.py +++ b/pyanaconda/ui/gui/spokes/installation_source.py @@ -28,7 +28,7 @@ SOURCE_TYPE_CLOSEST_MIRROR, SOURCE_TYPE_CDN, PAYLOAD_STATUS_SETTING_SOURCE, \ PAYLOAD_STATUS_INVALID_SOURCE, PAYLOAD_STATUS_CHECKING_SOFTWARE, SOURCE_TYPE_REPO_PATH, \ DRACUT_REPO_DIR -from pyanaconda.core.i18n import _, CN_ +from pyanaconda.core.i18n import _, CN_, C_ from pyanaconda.core.path import join_paths from pyanaconda.core.payload import parse_nfs_url, create_nfs_url, parse_hdd_url from pyanaconda.core.regexes import URL_PARSE, HOSTNAME_PATTERN_WITHOUT_ANCHORS @@ -122,7 +122,7 @@ def apply(self): # attached there is no need to refresh the installation source, # as without the subscription tokens the refresh would fail anyway. if cdn_source and not self.subscribed: - log.debug("CDN source but no subscribtion attached - skipping payload restart.") + log.debug("CDN source but no subscription attached - skipping payload restart.") elif source_changed or repo_changed or self._error: payloadMgr.start(self.payload) else: @@ -311,6 +311,22 @@ def subscribed(self): subscribed = subscription_proxy.IsSubscriptionAttached return subscribed + @property + def registered_to_satellite(self): + """Report if the system is registered to a Satellite instance. + + NOTE: This will be always False when the Subscription + module is not available. + + :return: True if registered to Satellite, False otherwise + :rtype: bool + """ + registered_to_satellite = False + if is_module_available(SUBSCRIPTION): + subscription_proxy = SUBSCRIPTION.get_proxy() + registered_to_satellite = subscription_proxy.IsRegisteredToSatellite + return registered_to_satellite + @property def status(self): # When CDN is selected as installation source and system @@ -327,6 +343,11 @@ def status(self): source_proxy = self.payload.get_source_proxy() return source_proxy.Description + if cdn_source and self.subscribed and self.registered_to_satellite: + # override the regular CDN source name to make it clear Satellite + # provided repositories are being used + return _("Satellite") + if thread_manager.get(constants.THREAD_CHECK_SOFTWARE): return _(PAYLOAD_STATUS_CHECKING_SOFTWARE) @@ -725,6 +746,26 @@ def refresh(self): # Update the URL entry validation now that we're done messing with sensitivites self._update_url_entry_check() + # If subscription module is available we might need to refresh the label + # of the CDN/Satellite radio button, so that it properly describes what is providing + # the repositories available after registration. + # + # For registration to Red Hat hosted infrastructure (also called Hosted Candlepin) the + # global Red Hat CDN efficiently provides quick access to the repositories to customers + # across the world over the public Internet. + # + # If registered to a customer Satellite instance, it is the Satellite instance itself that + # provides the software repositories. + # + # This is an important distinction as Satellite instances are often used in environments + # not connected to the public Internet, so seeing the installation source being provided + # by Red Hat CDN which the machine might not be able to reach could be very confusing. + if is_module_available(SUBSCRIPTION): + if self.registered_to_satellite: + self._cdn_button.set_label(C_("GUI|Software Source", "_Satellite")) + else: + self._cdn_button.set_label(C_("GUI|Software Source", "Red Hat _CDN")) + # Show the info bar with an error message if any. # This error message has the highest priority. if self._error: diff --git a/pyanaconda/ui/gui/spokes/lib/subscription.py b/pyanaconda/ui/gui/spokes/lib/subscription.py index 47b96b57792..578eb6f0a12 100644 --- a/pyanaconda/ui/gui/spokes/lib/subscription.py +++ b/pyanaconda/ui/gui/spokes/lib/subscription.py @@ -17,11 +17,6 @@ # Red Hat, Inc. # -import gi -gi.require_version("Gtk", "3.0") -gi.require_version("Pango", "1.0") -from gi.repository import Gtk, Pango - from collections import namedtuple from pyanaconda.core.i18n import _ @@ -108,129 +103,3 @@ def fill_combobox(combobox, user_provided_value, valid_values): # set the active id (what item should be selected in the combobox) combobox.set_active_id(active_id) - - -def add_attached_subscription_delegate(listbox, subscription, delegate_index): - """Add delegate representing an attached subscription to the listbox. - - :param listbox: a listbox to add the delegate to - :type listbox: GTK ListBox - :param subscription: a subscription attached to the system - :type: AttachedSubscription instance - :param int delegate_index: index of the delegate in the listbox - """ - log.debug("Subscription GUI: adding subscription to listbox: %s", subscription.name) - # if we are not the first delegate, we should pre-pend a spacer, so that the - # actual delegates are nicely delimited - if delegate_index != 0: - row = Gtk.ListBoxRow() - row.set_name("subscriptions_listbox_row_spacer") - row.set_margin_top(4) - listbox.insert(row, -1) - - # construct delegate - row = Gtk.ListBoxRow() - # set a name so that the ListBoxRow instance can be styled via CSS - row.set_name("subscriptions_listbox_row") - - main_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - main_vbox.set_margin_top(12) - main_vbox.set_margin_bottom(12) - - name_label = Gtk.Label(label='{}'.format(subscription.name), - use_markup=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, - hexpand=True, xalign=0, yalign=0.5) - name_label.set_margin_start(12) - name_label.set_margin_bottom(12) - - # create the first details grid - details_grid_1 = Gtk.Grid() - details_grid_1.set_column_spacing(12) - details_grid_1.set_row_spacing(12) - - # first column - service_level_label = Gtk.Label(label="{}".format(_("Service level")), - use_markup=True, xalign=0) - service_level_status_label = Gtk.Label(label=subscription.service_level) - sku_label = Gtk.Label(label="{}".format(_("SKU")), - use_markup=True, xalign=0) - sku_status_label = Gtk.Label(label=subscription.sku, xalign=0) - contract_label = Gtk.Label(label="{}".format(_("Contract")), - use_markup=True, xalign=0) - contract_status_label = Gtk.Label(label=subscription.contract, xalign=0) - - # add first column to the grid - details_grid_1.attach(service_level_label, 0, 0, 1, 1) - details_grid_1.attach(service_level_status_label, 1, 0, 1, 1) - details_grid_1.attach(sku_label, 0, 1, 1, 1) - details_grid_1.attach(sku_status_label, 1, 1, 1, 1) - details_grid_1.attach(contract_label, 0, 2, 1, 1) - details_grid_1.attach(contract_status_label, 1, 2, 1, 1) - - # second column - start_date_label = Gtk.Label(label="{}".format(_("Start date")), - use_markup=True, xalign=0) - start_date_status_label = Gtk.Label(label=subscription.start_date, xalign=0) - end_date_label = Gtk.Label(label="{}".format(_("End date")), - use_markup=True, xalign=0) - end_date_status_label = Gtk.Label(label=subscription.end_date, xalign=0) - entitlements_label = Gtk.Label(label="{}".format(_("Entitlements")), - use_markup=True, xalign=0) - entitlement_string = _("{} consumed").format(subscription.consumed_entitlement_count) - entitlements_status_label = Gtk.Label(label=entitlement_string, xalign=0) - - # create the second details grid - details_grid_2 = Gtk.Grid() - details_grid_2.set_column_spacing(12) - details_grid_2.set_row_spacing(12) - - # add second column to the grid - details_grid_2.attach(start_date_label, 0, 0, 1, 1) - details_grid_2.attach(start_date_status_label, 1, 0, 1, 1) - details_grid_2.attach(end_date_label, 0, 1, 1, 1) - details_grid_2.attach(end_date_status_label, 1, 1, 1, 1) - details_grid_2.attach(entitlements_label, 0, 2, 1, 1) - details_grid_2.attach(entitlements_status_label, 1, 2, 1, 1) - - details_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) - details_hbox.pack_start(details_grid_1, True, True, 12) - details_hbox.pack_start(details_grid_2, True, True, 0) - - main_vbox.pack_start(name_label, True, True, 0) - main_vbox.pack_start(details_hbox, True, True, 0) - - row.add(main_vbox) - - # append delegate to listbox - listbox.insert(row, -1) - - -def populate_attached_subscriptions_listbox(listbox, attached_subscriptions): - """Populate the attached subscriptions listbox with delegates. - - Unfortunately it does not seem to be possible to create delegate templates - that could be reused for each data item in the listbox via Glade, so - we need to construct them imperatively via Python GTK API. - - :param listbox: listbox to populate - :type listbox: GTK ListBox - :param attached_subscriptions: list of AttachedSubscription instances - """ - log.debug("Subscription GUI: populating attached subscriptions listbox") - - # start by making sure the listbox is empty - for child in listbox.get_children(): - listbox.remove(child) - del(child) - - # add one delegate per attached subscription - delegate_index = 0 - for subscription in attached_subscriptions: - add_attached_subscription_delegate(listbox, subscription, delegate_index) - delegate_index = delegate_index + 1 - - # Make sure the delegates are actually visible after the listbox has been cleared. - # Without show_all() nothing would be visible past first clear. - listbox.show_all() - - log.debug("Subscription GUI: attached subscriptions listbox has been populated") diff --git a/pyanaconda/ui/gui/spokes/subscription.glade b/pyanaconda/ui/gui/spokes/subscription.glade index b3d4058f47e..925fc496191 100644 --- a/pyanaconda/ui/gui/spokes/subscription.glade +++ b/pyanaconda/ui/gui/spokes/subscription.glade @@ -1,26 +1,27 @@ - + - False - CONNECT TO RED HAT + False + CONNECT TO RED HAT - False + False vertical 6 - False + False + - False - 6 - 6 - 6 + False + 6 + 6 + 6 @@ -32,53 +33,54 @@ - False + False 0 - 12 - 48 - 48 - 48 + 12 + 48 + 48 + 48 - False + False vertical 8 True - False + False True - False - False + False + False True - True + True True - False + False center - none + none True - False + False center vertical 4 + True - False + False center center - 4 - 4 + 4 + 4 True - False + False end Authentication right @@ -87,23 +89,23 @@ - 0 - 0 + 0 + 0 True - False + False _Account True - True - False - True + True + False + True True - True + True @@ -116,10 +118,10 @@ Activation _Key True - True - False - True - True + True + False + True + True account_radio_button @@ -131,157 +133,217 @@ - 1 - 0 + 1 + 0 True - False - none - True + False + none + True + True - False + False True - 4 - 4 + 4 + 4 True - False + False start User name - 0 - 0 + 0 + 0 - 250 + 250 True - True + True True - 1 - 0 + 1 + 0 True - False + False start Password - 0 - 1 + 0 + 1 True - True + True True False - + - 1 - 1 + 1 + 1 + + + True + False + none + + + False + Organization ID + + + + + 0 + 2 + + + + + True + False + none + + + False + + + + + + 1 + 2 + + + + + + + + + + + - 1 - 1 + 1 + 1 True - False - none + False + none + True - False + False True - 4 - 4 + 4 + 4 True - False + False start - Organization + Organization ID - 0 - 0 + 0 + 0 True - False + False start Activation Key - 0 - 1 + 0 + 1 True - True + True True - 1 - 0 + 1 + 0 True - True + True True - key1,key2,... + key1,key2,... - 1 - 1 + 1 + 1 + + + + + + + + + + + + + + + - 1 - 2 + 1 + 2 True - False + False end Purpose right @@ -290,115 +352,125 @@ - 0 - 3 + 0 + 3 True - False + False + True - False - 4 - 4 + False + 4 + 4 True - False + False start Usage - 0 - 2 + 0 + 2 True - False + False start Role - 0 - 0 + 0 + 0 True - False + False start SLA - 0 - 1 + 0 + 1 True - False + False - 1 - 0 + 1 + 0 True - False + False - 1 - 1 + 1 + 1 True - False + False - 1 - 2 + 1 + 2 + + + + + + + + + - 1 - 4 + 1 + 4 Set System Purpose True - True - False - True + True + False + True - 1 - 3 + 1 + 3 True - False + False end Insights right @@ -407,24 +479,24 @@ - 0 - 5 + 0 + 5 Connect to Red Hat _Insights True - True - False - Red Hat Insights aims to increase your IT efficiency and speed across hybrid infrastructures by identifying and prioritizing risks, managing vulnerabilities, and compliance, and analyzing costs. For more information, visit the Red Hat Insights information page. - True + True + False + True + Red Hat Insights aims to increase your IT efficiency and speed across hybrid infrastructures by identifying and prioritizing risks, managing vulnerabilities, and compliance, and analyzing costs. For more information, visit the Red Hat Insights information page. True - True + True - 1 - 5 + 1 + 5 @@ -436,6 +508,24 @@ + + + + + + + + + + + + + + + + + + False @@ -446,181 +536,192 @@ True - True + True + True - False - 4 - 4 + False + 4 + 4 Custom base URL True - True - False - True + True + False + True - 1 - 4 + 1 + 4 True - False + False True - True + True - 1 - 5 + 1 + 5 True - False + False True - True + True - 1 - 3 + 1 + 3 - Custom server URL + Satellite URL True - True - False - True + True + False + True - 1 - 2 + 1 + 2 Use HTTP proxy True - True - False - True + True + False + True - 1 - 0 + 1 + 0 True - False + False + True - False - 4 - 4 + False + 4 + 4 True - False + False start Location - 0 - 0 + 0 + 0 True - False + False start User name - 0 - 1 + 0 + 1 True - False + False start Password - 0 - 2 + 0 + 2 True - True + True True - hostname:port + hostname:port - 1 - 0 + 1 + 0 - 250 + 250 True - True + True - 1 - 1 + 1 + 1 True - True + True False - + - 1 - 2 + 1 + 2 + + + + + + + + + - 1 - 1 + 1 + 1 @@ -641,16 +742,34 @@ + + + + + + + + + + + + + + + + + + True - False + False True - False + False Options @@ -671,9 +790,9 @@ True - False - 8 - 8 + False + 8 + 8 The system is currently not registered. @@ -690,10 +809,10 @@ _Register True False - True - True + True + True center - True + True @@ -714,26 +833,26 @@ True - False - 8 + False + 8 vertical 4 True - False + False vertical 4 True - False + False 4 - + True - False - The system has been properly subscribed + False + "" @@ -749,10 +868,10 @@ _Unregister True - True - True + True + True center - True + True @@ -770,18 +889,19 @@ + True - False + False start - 8 - 4 - 8 + 8 + 4 + 8 method_label True - False + False end Method right @@ -790,15 +910,15 @@ - 0 - 0 + 0 + 0 system_purpose_label True - False + False end System Purpose right @@ -807,51 +927,51 @@ - 0 - 1 + 0 + 1 True - False + False start lorem ipsum - 1 - 0 + 1 + 0 True - False + False start lorem ipsum - 1 - 1 + 1 + 1 True - False + False start lorem ipsum - 1 - 2 + 1 + 2 insights_label True - False + False end Insights right @@ -860,32 +980,32 @@ - 0 - 4 + 0 + 4 True - False + False start lorem ipsum - 1 - 4 + 1 + 4 True - False + False start lorem ipsum - 1 - 3 + 1 + 3 @@ -894,6 +1014,21 @@ + + + + + + + + + + + + + + + False @@ -911,10 +1046,10 @@ True - False + False start - 16 - 4 + 16 + 4 No subscriptions have been attached to the system @@ -930,18 +1065,18 @@ True - True + True True True - False - none + False + none True - False - none + False + none diff --git a/pyanaconda/ui/gui/spokes/subscription.py b/pyanaconda/ui/gui/spokes/subscription.py index e8cf44249dc..16c4af8021b 100644 --- a/pyanaconda/ui/gui/spokes/subscription.py +++ b/pyanaconda/ui/gui/spokes/subscription.py @@ -19,6 +19,8 @@ from enum import IntEnum +from dasbus.typing import unwrap_variant + from pyanaconda.flags import flags from pyanaconda.core.threads import thread_manager @@ -34,13 +36,13 @@ from pyanaconda.modules.common.constants.services import SUBSCRIPTION, NETWORK from pyanaconda.modules.common.structures.subscription import SystemPurposeData, \ - SubscriptionRequest, AttachedSubscription + SubscriptionRequest, OrganizationData +from pyanaconda.modules.common.errors.subscription import MultipleOrganizationsError from pyanaconda.modules.common.util import is_module_available -from pyanaconda.modules.common.task import sync_run_task +from pyanaconda.modules.common.task import sync_run_task, async_run_task from pyanaconda.ui.gui.spokes import NormalSpoke -from pyanaconda.ui.gui.spokes.lib.subscription import fill_combobox, \ - populate_attached_subscriptions_listbox +from pyanaconda.ui.gui.spokes.lib.subscription import fill_combobox from pyanaconda.ui.gui.utils import set_password_visibility from pyanaconda.ui.categories.software import SoftwareCategory from pyanaconda.ui.communication import hubQ @@ -315,6 +317,9 @@ def on_activation_key_radio_button_toggled(self, radio): def on_username_entry_changed(self, editable): self.subscription_request.account_username = editable.get_text() self._update_registration_state() + # changes to username can invalidate the organization list, + # so hide it if the username changes + self._disable_org_selection_for_account() def on_password_entry_changed(self, editable): entered_text = editable.get_text() @@ -337,6 +342,11 @@ def on_password_entry_map(self, entry): """ set_password_visibility(entry, False) + def on_select_organization_combobox_changed(self, combobox): + log.debug("Subscription GUI: organization selected for account: %s", + combobox.get_active_id()) + self.subscription_request.account_organization = combobox.get_active_id() + def on_organization_entry_changed(self, editable): self.subscription_request.organization = editable.get_text() self._update_registration_state() @@ -561,6 +571,17 @@ def initialize(self): self._username_entry = self.builder.get_object("username_entry") self._password_entry = self.builder.get_object("password_entry") + # authentication - account - org selection + self._select_organization_label_revealer = self.builder.get_object( + "select_organization_label_revealer" + ) + self._select_organization_combobox_revealer = self.builder.get_object( + "select_organization_combobox_revealer" + ) + self._select_organization_combobox = self.builder.get_object( + "select_organization_combobox" + ) + # authentication - activation key self._activation_key_revealer = self.builder.get_object("activation_key_revealer") self._organization_entry = self.builder.get_object("organization_entry") @@ -626,6 +647,7 @@ def initialize(self): # * the subscription status tab * # # general status + self._subscription_status_label = self.builder.get_object("subscription_status_label") self._method_status_label = self.builder.get_object("method_status_label") self._role_status_label = self.builder.get_object("role_status_label") self._sla_status_label = self.builder.get_object("sla_status_label") @@ -965,21 +987,65 @@ def _subscription_progress_callback(self, phase): self._update_registration_state() @async_action_wait - def _subscription_error_callback(self, error_message): + def _subscription_error_callback(self, error): log.debug("Subscription GUI: registration & attach failed") # store the error message - self.registration_error = error_message + self.registration_error = str(error) # even if we fail, we are technically done, # so clear the phase self.registration_phase = None # update registration and subscription parts of the spoke self._update_registration_state() self._update_subscription_state() + # if the error is an instance of multi-org error, + # fetch organization list & enable org selection + # checkbox + if isinstance(error, MultipleOrganizationsError): + task_path = self._subscription_module.RetrieveOrganizationsWithTask() + task_proxy = SUBSCRIPTION.get_proxy(task_path) + async_run_task(task_proxy, self._process_org_list) # re-enable controls, so user can try again self.set_registration_controls_sensitive(True) # notify hub hubQ.send_ready(self.__class__.__name__) + def _process_org_list(self, task_proxy): + """Process org listing for account. + + Called as an async callback of the organization listing runtime task. + + :param task_proxy: a task + """ + # finish the task + task_proxy.Finish() + # process the organization list + org_struct_list = unwrap_variant(task_proxy.GetResult()) + org_list = OrganizationData.from_structure_list(org_struct_list) + # fill the combobox + self._select_organization_combobox.remove_all() + # also add a placeholder and make it the active item so it is visible + self._select_organization_combobox.append("", _("Not Specified")) + self._select_organization_combobox.set_active_id("") + for org in org_list: + self._select_organization_combobox.append(org.id, org.name) + # show the combobox + self._enable_org_selection_for_account() + + def _enable_org_selection_for_account(self): + self._select_organization_label_revealer.set_reveal_child(True) + self._select_organization_combobox_revealer.set_reveal_child(True) + + def _disable_org_selection_for_account(self): + """Disable the org selection combobox. + + And also wipe the last used organization id or else it might be used + for the next registration attempt with a different username, + triggering confusing authetication failures. + """ + self._subscription_request.account_organization = "" + self._select_organization_label_revealer.set_reveal_child(False) + self._select_organization_combobox_revealer.set_reveal_child(False) + def _get_status_message(self): """Get status message describing current spoke state. @@ -1003,7 +1069,10 @@ def _get_status_message(self): elif self.registration_error: return _("Registration failed.") elif self.subscription_attached: - return _("Registered.") + if self._subscription_module.IsRegisteredToSatellite: + return _("Registered to Satellite.") + else: + return _("Registered.") else: return _("Not registered.") @@ -1034,6 +1103,16 @@ def _update_subscription_state(self): Update state of the part of the spoke, that shows data about the currently attached subscriptions. """ + # top level status label + if self._subscription_module.IsRegisteredToSatellite: + self._subscription_status_label.set_text( + _("The system is registered to a Satellite instance.") + ) + else: + self._subscription_status_label.set_text( + _("The system is registered.") + ) + # authentication method if self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD: method_string = _("Registered with account {}").format( @@ -1069,30 +1148,9 @@ def _update_subscription_state(self): insights_string = _("Not connected to Red Hat Insights") self._insights_status_label.set_text(insights_string) - # get attached subscriptions as a list of structs - attached_subscriptions = self._subscription_module.AttachedSubscriptions - # turn the structs to more useful AttachedSubscription instances - attached_subscriptions = AttachedSubscription.from_structure_list(attached_subscriptions) - - # check how many we have & set the subscription status string accordingly - subscription_count = len(attached_subscriptions) - if subscription_count == 0: - subscription_string = _("No subscriptions are attached to the system") - elif subscription_count == 1: - subscription_string = _("1 subscription attached to the system") - else: - subscription_string = _("{} subscriptions attached to the system").format( - subscription_count - ) - + subscription_string = _("Subscribed in Simple Content Access mode.") self._attached_subscriptions_label.set_text(subscription_string) - # populate the attached subscriptions listbox - populate_attached_subscriptions_listbox( - self._subscriptions_listbox, - attached_subscriptions - ) - def _check_connectivity(self): """Check network connectivity is available.