def refresh(self): self._sectorSizeCombo.set_active(0) if self._namespaces: self._devicesLabel.set_text("%s" % ", ".join(self._namespaces)) else: self._sectorSizeCombo.set_sensitive(False) self._okButton.set_sensitive(False) self._startButton.set_sensitive(False) self._sectorSizeLabel.set_sensitive(False) self._infoLabel.set_text( CN_("GUI|Advanced Storage|NVDIM", "No device to be reconfigured selected."))
def refresh(self): self._sectorSizeSpinButton.set_value(DEFAULT_SECTOR_SIZE) if self.namespaces: self._devicesLabel.set_text("%s" % ", ".join(self.namespaces)) else: msg = CN_("GUI|Advanced Storage|NVDIM", "No device to be reconfigured selected.") self._infoLabel.set_text(msg) for widget in [ self._sectorSizeSpinButton, self._okButton, self._startButton, self._sectorSizeLabel ]: widget.set_sensitive(False)
class LangsupportSpoke(NormalSpoke, LangLocaleHandler): """ .. inheritance-diagram:: LangsupportSpoke :parts: 3 """ builderObjects = [ "languageStore", "languageStoreFilter", "localeStore", "langsupportWindow" ] mainWidgetName = "langsupportWindow" focusWidgetName = "languageEntry" uiFile = "spokes/language_support.glade" category = LocalizationCategory icon = "accessories-character-map-symbolic" title = CN_("GUI|Spoke", "_Language Support") @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "language-configuration" @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(LOCALIZATION): return False if not NormalSpoke.should_run(environment, data): return False # Don't show the language support spoke on live media. return context.payload_type not in PAYLOAD_LIVE_TYPES def __init__(self, *args, **kwargs): NormalSpoke.__init__(self, *args, **kwargs) LangLocaleHandler.__init__(self) self._selected_locales = set() self._l12_module = LOCALIZATION.get_proxy() def initialize(self): self.initialize_start() self._languageStore = self.builder.get_object("languageStore") self._languageEntry = self.builder.get_object("languageEntry") self._languageStoreFilter = self.builder.get_object( "languageStoreFilter") self._langView = self.builder.get_object("languageView") self._langSelectedRenderer = self.builder.get_object( "langSelectedRenderer") self._langSelectedColumn = self.builder.get_object( "langSelectedColumn") self._langSelection = self.builder.get_object("languageViewSelection") self._localeStore = self.builder.get_object("localeStore") self._localeView = self.builder.get_object("localeView") LangLocaleHandler.initialize(self) # mark selected locales and languages with selected locales bold localeNativeColumn = self.builder.get_object("localeNativeName") localeNativeNameRenderer = self.builder.get_object( "localeNativeNameRenderer") override_cell_property(localeNativeColumn, localeNativeNameRenderer, "weight", self._mark_selected_locale_bold) languageNameColumn = self.builder.get_object("nameColumn") nativeNameRenderer = self.builder.get_object("nativeNameRenderer") englishNameRenderer = self.builder.get_object("englishNameRenderer") override_cell_property(languageNameColumn, nativeNameRenderer, "weight", self._mark_selected_language_bold) override_cell_property(languageNameColumn, englishNameRenderer, "weight", self._mark_selected_language_bold) # If a language has selected locales, highlight every column so that # the row appears highlighted for col in self._langView.get_columns(): for rend in col.get_cells(): override_cell_property(col, rend, "cell-background-rgba", self._highlight_selected_language) # and also set an icon so that we don't depend on a color to convey information highlightedColumn = self.builder.get_object("highlighted") highlightedRenderer = self.builder.get_object("highlightedRenderer") override_cell_property(highlightedColumn, highlightedRenderer, "icon-name", self._render_lang_highlighted) # report that we are done self.initialize_done() def apply(self): # store only additional langsupport locales added = sorted(self._selected_locales - set([self._l12_module.Language])) self._l12_module.SetLanguageSupport(added) def refresh(self): self._languageEntry.set_text("") self._selected_locales = set(self._installed_langsupports) # select the first locale from the "to be installed" langsupports self._select_locale(self._installed_langsupports[0]) @property def _installed_langsupports(self): return [self._l12_module.Language] + sorted( self._l12_module.LanguageSupport) @property def status(self): return ", ".join( localization.get_native_name(locale) for locale in self._installed_langsupports) @property def mandatory(self): return False @property def completed(self): return True def _add_language(self, store, native, english, lang): native_span = '<span lang="%s">%s</span>' % \ (escape_markup(lang), escape_markup(native)) store.append([native_span, english, lang]) def _add_locale(self, store, native, locale): native_span = '<span lang="%s">%s</span>' % \ (escape_markup(re.sub(r'\..*', '', locale)), escape_markup(native)) # native, locale, selected, additional store.append([ native_span, locale, locale in self._selected_locales, locale != self._l12_module.Language ]) def _mark_selected_locale_bold(self, column, renderer, model, itr, user_data=None): if model[itr][2]: return Pango.Weight.BOLD.real else: return Pango.Weight.NORMAL.real def _is_lang_selected(self, lang): lang_locales = set(localization.get_language_locales(lang)) return not lang_locales.isdisjoint(self._selected_locales) def _mark_selected_language_bold(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return Pango.Weight.BOLD.real else: return Pango.Weight.NORMAL.real def _highlight_selected_language(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return _HIGHLIGHT_COLOR else: return None def _render_lang_highlighted(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return "emblem-ok-symbolic" else: return None # Signal handlers. def on_locale_toggled(self, renderer, path): itr = self._localeStore.get_iter(path) row = self._localeStore[itr] row[2] = not row[2] if row[2]: self._selected_locales.add(row[1]) else: self._selected_locales.remove(row[1])
class KeyboardSpoke(NormalSpoke): """ .. inheritance-diagram:: KeyboardSpoke :parts: 3 """ builderObjects = ["addedLayoutStore", "keyboardWindow", "layoutTestBuffer"] mainWidgetName = "keyboardWindow" uiFile = "spokes/keyboard.glade" category = LocalizationCategory icon = "input-keyboard-symbolic" title = CN_("GUI|Spoke", "_Keyboard") @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "keyboard-configuration" @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(LOCALIZATION): return False return NormalSpoke.should_run(environment, data) def __init__(self, *args): super().__init__(*args) self._remove_last_attempt = False self._confirmed = False self._xkl_wrapper = XklWrapper.get_instance() self._add_dialog = None self._ready = False self._upButton = self.builder.get_object("upButton") self._downButton = self.builder.get_object("downButton") self._removeButton = self.builder.get_object("removeLayoutButton") self._previewButton = self.builder.get_object("previewButton") self._l12_module = LOCALIZATION.get_proxy() self._seen = self._l12_module.KeyboardKickstarted def apply(self): # the user has confirmed (seen) the configuration self._confirmed = True self._seen = True # Update module with actual values layouts = [row[0] for row in self._store] self._l12_module.SetXLayouts(layouts) @property def completed(self): if flags.flags.automatedInstall and not self._seen: return False # The currently activated layout is a different from the # selected ones. Ignore VNC, since VNC keymaps are weird # and more on the client side. if not self._confirmed and not flags.flags.usevnc \ and self._xkl_wrapper.get_current_layout() not in self._l12_module.XLayouts: return False return True @property def status(self): # We don't need to check that self._store is empty, because that isn't allowed. descriptions = (self._xkl_wrapper.get_layout_variant_description( row[0]) for row in self._store) return ", ".join(descriptions) @property def ready(self): return self._ready and threadMgr.get( ADD_LAYOUTS_INITIALIZE_THREAD) is None def initialize(self): super().initialize() self.initialize_start() # set X keyboard defaults # - this needs to be done early in spoke initialization so that # the spoke status does not show outdated keyboard selection keyboard.set_x_keyboard_defaults(self._l12_module, self._xkl_wrapper) # make sure the x_layouts list has at least one keyboard layout if not self._l12_module.XLayouts: self._l12_module.SetXLayouts([DEFAULT_KEYBOARD]) self._add_dialog = AddLayoutDialog(self.data) self._add_dialog.initialize() if conf.system.can_configure_keyboard: self.builder.get_object("warningBox").hide() # We want to store layouts' names but show layouts as # 'language (description)'. layoutColumn = self.builder.get_object("layoutColumn") layoutRenderer = self.builder.get_object("layoutRenderer") override_cell_property(layoutColumn, layoutRenderer, "text", _show_layout, self._xkl_wrapper) self._store = self.builder.get_object("addedLayoutStore") self._add_data_layouts() self._selection = self.builder.get_object("layoutSelection") self._switching_dialog = ConfigureSwitchingDialog( self.data, self._l12_module) self._switching_dialog.initialize() self._layoutSwitchLabel = self.builder.get_object("layoutSwitchLabel") if not conf.system.can_configure_keyboard: # Disable area for testing layouts as we cannot make # it work without modifying runtime system widgets = [ self.builder.get_object("testingLabel"), self.builder.get_object("testingWindow"), self.builder.get_object("layoutSwitchLabel") ] # Use testingLabel's text to explain why this part is not # sensitive. widgets[0].set_text( _("Testing layouts configuration not " "available.")) for widget in widgets: widget.set_sensitive(False) hubQ.send_not_ready(self.__class__.__name__) hubQ.send_message(self.__class__.__name__, _("Getting list of layouts...")) threadMgr.add( AnacondaThread(name=THREAD_KEYBOARD_INIT, target=self._wait_ready)) def _wait_ready(self): self._add_dialog.wait_initialize() self._ready = True hubQ.send_ready(self.__class__.__name__) # report that the keyboard spoke initialization has been completed self.initialize_done() def refresh(self): super().refresh() # Clear out the layout testing box every time the spoke is loaded. It # doesn't make sense to leave temporary data laying around. buf = self.builder.get_object("layoutTestBuffer") buf.set_text("") # Clear and repopulate addedLayoutStore with values from the module data self._store.clear() self._add_data_layouts() # Start with no buttons enabled, since nothing is selected. self._upButton.set_sensitive(False) self._downButton.set_sensitive(False) self._removeButton.set_sensitive(False) self._previewButton.set_sensitive(False) self._refresh_switching_info() def _addLayout(self, store, name): # first try to add the layout if conf.system.can_configure_keyboard: self._xkl_wrapper.add_layout(name) # valid layout, append it to the store store.append([name]) def _removeLayout(self, store, itr): """ Remove the layout specified by store iterator from the store and X runtime configuration. """ if conf.system.can_configure_keyboard: self._xkl_wrapper.remove_layout(store[itr][0]) store.remove(itr) def _refresh_switching_info(self): switch_options = self._l12_module.LayoutSwitchOptions if flags.flags.usevnc: self._layoutSwitchLabel.set_text( _("Keyboard layouts are not " "supported when using VNC.\n" "However the settings will be used " "after the installation.")) elif switch_options: first_option = switch_options[0] desc = self._xkl_wrapper.get_switch_opt_description(first_option) self._layoutSwitchLabel.set_text(_(LAYOUT_SWITCHING_INFO) % desc) else: self._layoutSwitchLabel.set_text( _("Layout switching not " "configured.")) # Signal handlers. def on_add_clicked(self, button): self._add_dialog.refresh() with self.main_window.enlightbox(self._add_dialog.window): response = self._add_dialog.run() if response == 1: duplicates = set() for row in self._store: item = row[0] if item in self._add_dialog.chosen_layouts: duplicates.add(item) for layout in self._add_dialog.chosen_layouts: if layout not in duplicates: self._addLayout(self._store, layout) if self._remove_last_attempt: itr = self._store.get_iter_first() if not self._store[itr][0] in self._add_dialog.chosen_layouts: self._removeLayout(self._store, itr) self._remove_last_attempt = False # Update the selection information self._selection.emit("changed") def on_remove_clicked(self, button): if not self._selection.count_selected_rows(): return (store, itr) = self._selection.get_selected() itr2 = store.get_iter_first() #if the first item is selected, try to select the next one if store[itr][0] == store[itr2][0]: itr2 = store.iter_next(itr2) if itr2: #next one existing self._selection.select_iter(itr2) self._removeLayout(store, itr) # Re-emit the selection changed signal now that the backing store is updated # in order to update the first/last/only-based button sensitivities self._selection.emit("changed") return #nothing left, run AddLayout dialog to replace the current layout #add it to GLib.idle to make sure the underlaying gui is correctly #redrawn self._remove_last_attempt = True add_button = self.builder.get_object("addLayoutButton") gtk_call_once(self.on_add_clicked, add_button) return #the selected item is not the first, select the previous one #XXX: there is no model.iter_previous() so we have to find it this way itr3 = store.iter_next(itr2) #look-ahead iterator while itr3 and (store[itr3][0] != store[itr][0]): itr2 = store.iter_next(itr2) itr3 = store.iter_next(itr3) self._removeLayout(store, itr) self._selection.select_iter(itr2) def on_up_clicked(self, button): if not self._selection.count_selected_rows(): return (store, cur) = self._selection.get_selected() prev = store.iter_previous(cur) if not prev: return store.swap(cur, prev) if conf.system.can_configure_keyboard: self._flush_layouts_to_X() if not store.iter_previous(cur): #layout is first in the list (set as default), activate it self._xkl_wrapper.activate_default_layout() self._selection.emit("changed") def on_down_clicked(self, button): if not self._selection.count_selected_rows(): return (store, cur) = self._selection.get_selected() #if default layout (first in the list) changes we need to activate it activate_default = not store.iter_previous(cur) nxt = store.iter_next(cur) if not nxt: return store.swap(cur, nxt) if conf.system.can_configure_keyboard: self._flush_layouts_to_X() if activate_default: self._xkl_wrapper.activate_default_layout() self._selection.emit("changed") def on_preview_clicked(self, button): (store, cur) = self._selection.get_selected() layout_row = store[cur] if not layout_row: return layout, variant = keyboard.parse_layout_variant(layout_row[0]) if variant: lay_var_spec = "%s\t%s" % (layout, variant) else: lay_var_spec = layout dialog = Gkbd.KeyboardDrawing.dialog_new() Gkbd.KeyboardDrawing.dialog_set_layout(dialog, self._xkl_wrapper.configreg, lay_var_spec) dialog.set_size_request(750, 350) dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS) with self.main_window.enlightbox(dialog): dialog.show_all() dialog.run() dialog.destroy() def on_selection_changed(self, selection, *args): # We don't have to worry about multiple rows being selected in this # function, because that's disabled by the widget. if not selection.count_selected_rows(): self._upButton.set_sensitive(False) self._downButton.set_sensitive(False) self._removeButton.set_sensitive(False) self._previewButton.set_sensitive(False) return (store, selected) = selection.get_selected_rows() # If something's selected, always enable the remove and preview buttons. self._removeButton.set_sensitive(True) self._previewButton.set_sensitive(True) # If only one row is available, disable both the Up and Down button if len(store) == 1: self._upButton.set_sensitive(False) self._downButton.set_sensitive(False) else: # Disable the Up button if the top row's selected, and disable the # Down button if the bottom row's selected. if selected[0].get_indices() == [0]: self._upButton.set_sensitive(False) self._downButton.set_sensitive(True) elif selected[0].get_indices() == [len(store) - 1]: self._upButton.set_sensitive(True) self._downButton.set_sensitive(False) else: self._upButton.set_sensitive(True) self._downButton.set_sensitive(True) def on_options_clicked(self, *args): self._switching_dialog.refresh() with self.main_window.enlightbox(self._switching_dialog.window): response = self._switching_dialog.run() if response != 1: # Cancel clicked, dialog destroyed return # OK clicked, set and save switching options. new_options = self._switching_dialog.checked_options self._xkl_wrapper.set_switching_options(new_options) self._l12_module.SetLayoutSwitchOptions(new_options) # Refresh switching info label. self._refresh_switching_info() def _add_data_layouts(self): if not self._l12_module.XLayouts: # nothing specified, just add the default self._addLayout(self._store, DEFAULT_KEYBOARD) return valid_layouts = [] for layout in self._l12_module.XLayouts: try: self._addLayout(self._store, layout) valid_layouts += layout except XklWrapperError: log.error("Failed to add layout '%s'", layout) if not valid_layouts: log.error("No valid layout given, falling back to default %s", DEFAULT_KEYBOARD) self._addLayout(self._store, DEFAULT_KEYBOARD) self._l12_module.SetXLayouts([DEFAULT_KEYBOARD]) def _flush_layouts_to_X(self): layouts_list = list() for row in self._store: layouts_list.append(row[0]) self._xkl_wrapper.replace_layouts(layouts_list)
class SubscriptionSpoke(NormalSpoke): """Subscription spoke provides the Connect to Red Hat screen.""" builderObjects = ["subscription_window"] mainWidgetName = "subscription_window" uiFile = "spokes/subscription.glade" category = SoftwareCategory icon = "application-certificate-symbolic" title = CN_("GUI|Spoke", "_Connect to Red Hat") # main notebook pages REGISTRATION_PAGE = 0 SUBSCRIPTION_STATUS_PAGE = 1 @classmethod def should_run(cls, environment, data): """The Subscription spoke should run only if the Subscription module is available.""" return is_module_available(SUBSCRIPTION) def __init__(self, *args): super().__init__(*args) # connect to the Subscription DBus module API self._subscription_module = SUBSCRIPTION.get_proxy() # connect to the Network DBus module API self._network_module = NETWORK.get_proxy() # get initial data from the Subscription module self._subscription_request = self._get_subscription_request() self._system_purpose_data = self._get_system_purpose_data() # Keep a copy of system purpose data that has been last applied to # the installation environment. # That way we can check if the main copy of the system purposed data # changed since it was applied (for example due to user input) # and needs to be reapplied. # By default this variable is None and will only be set to a # SystemPurposeData instance when first system purpose data is # applied to the installation environment. self._last_applied_system_purpose_data = None self._authentication_method = AuthenticationMethod.USERNAME_PASSWORD self._registration_error = "" self._registration_phase = None self._registration_controls_enabled = True # Red Hat Insights should be enabled by default for non-kickstart installs. # # For kickstart installations we will use the value from the module, which # False by default & can be set to True via the rhsm kickstart command. if not flags.automatedInstall: self._subscription_module.SetInsightsEnabled(True) # previous visit network connectivity tracking self._network_connected_previously = False # overriden source tracking self._overridden_source_type = None # common spoke properties @property def ready(self): """The subscription spoke is ready once its initialization thread finishes. We do this to avoid the Subscription spoke being set mandatory in cases where the current installation source is the CDN, but payload refresh is still running and it might change to CDROM later one. We achieve this by waiting for tha payload refresh thread to finish in the Subscription spoke initialization thread. """ return not threadMgr.get(THREAD_SUBSCRIPTION_SPOKE_INIT) @property def status(self): # The spoke status message: # - shows registration phases when registration + subscription # or unregistration is ongoing # - otherwise shows not-registered/registered/error return self._get_status_message() @property def mandatory(self): """The subscription spoke is mandatory if Red Hat CDN is set as installation source.""" return check_cdn_is_installation_source(self.payload) @property def completed(self): return self.subscription_attached @property def sensitive(self): # the Subscription spoke should be always accessible return True # common spoke methods def apply(self): log.debug("Subscription GUI: apply() running") self._set_data_to_module() def refresh(self): log.debug("Subscription GUI: refresh() running") # update spoke state based on up-to-date data from the Subscription module # (this also takes care of updating the two properties holding subscription # request as well as system purpose data) self._update_spoke_state() # check if network connectivity is available # - without network connectivity the spoke is pretty much unusable # - also, no need to check if registration/unregistration is in progress if not self.registration_phase: self._check_connectivity() # DBus structure mirrors @property def subscription_request(self): """A mirror of the subscription request from the Subscription DBus module. Should be always set and is periodically updated on refresh(). :return: up to date subscription request :rtype: SubscriptionRequest instance """ return self._subscription_request @property def system_purpose_data(self): """A mirror of system purpose data from the Subscription DBus module. Should be always set and is periodically updated on refresh(). :return: up to date system purpose data :rtype: SystemPurposeData instance """ return self._system_purpose_data # placeholder control def enable_http_proxy_password_placeholder(self, show_placeholder): """Show a placeholder on the HTTP proxy password field. The placeholder notifies the user about HTTP proxy password being set in the DBus module. The placeholder will be only shown if there is no actual text in the entry field. """ if show_placeholder: self._http_proxy_password_entry.set_placeholder_text( _("Password set.")) else: self._http_proxy_password_entry.set_placeholder_text("") def enable_password_placeholder(self, show_placeholder): """Show a placeholder on the red hat account password field. The placeholder notifies the user about activation key being set in the DBus module. The placeholder will be only shown if there is no actual text in the entry field. """ if show_placeholder: self._password_entry.set_placeholder_text(_("Password set.")) else: self._password_entry.set_placeholder_text("") def enable_activation_key_placeholder(self, show_placeholder): """Show a placeholder on the activation key field. The placeholder notifies the user about activation key being set in the DBus module. The placeholder will be only shown if there is no actual text in the entry field. """ if show_placeholder: self._activation_key_entry.set_placeholder_text( _("Activation key set.")) else: self._activation_key_entry.set_placeholder_text("") # properties controlling visibility of options that can be hidden @property def custom_server_hostname_visible(self): return self._custom_server_hostname_checkbox.get_active() @custom_server_hostname_visible.setter def custom_server_hostname_visible(self, visible): self._custom_server_hostname_checkbox.set_active(visible) @property def http_proxy_visible(self): return self._http_proxy_checkbox.get_active() @http_proxy_visible.setter def http_proxy_visible(self, visible): self._http_proxy_checkbox.set_active(visible) @property def custom_rhsm_baseurl_visible(self): return self._custom_rhsm_baseurl_checkbox.get_active() @custom_rhsm_baseurl_visible.setter def custom_rhsm_baseurl_visible(self, visible): self._custom_rhsm_baseurl_checkbox.set_active(visible) def set_account_visible(self, visible): self._account_radio_button.set_active(visible) def set_activation_key_visible(self, visible): self._activation_key_radio_button.set_active(visible) def set_system_purpose_visible(self, visible): self._system_purpose_checkbox.set_active(visible) def set_options_visible(self, visible): self._options_expander.set_expanded(visible) # properties - element sensitivity def set_registration_controls_sensitive(self, sensitive): """Set sensitivity of the registration controls. We set these value individually so that the registration status label that is between the controls will not become grayed out due to setting the top level container insensitive. """ self._registration_grid.set_sensitive(sensitive) self._options_expander.set_sensitive(sensitive) self._registration_controls_enabled = sensitive self._update_registration_state() # authentication related signals def on_account_radio_button_toggled(self, radio): self._account_revealer.set_reveal_child(radio.get_active()) if radio.get_active(): self.authentication_method = AuthenticationMethod.USERNAME_PASSWORD def on_activation_key_radio_button_toggled(self, radio): self._activation_key_revealer.set_reveal_child(radio.get_active()) if radio.get_active(): self.authentication_method = AuthenticationMethod.ORG_KEY def on_username_entry_changed(self, editable): self.subscription_request.account_username = editable.get_text() self._update_registration_state() def on_password_entry_changed(self, editable): entered_text = editable.get_text() if entered_text: self.enable_password_placeholder(False) self.subscription_request.account_password.set_secret(entered_text) self._update_registration_state() def on_organization_entry_changed(self, editable): self.subscription_request.organization = editable.get_text() self._update_registration_state() def on_activation_key_entry_changed(self, editable): entered_text = editable.get_text() keys = None if entered_text: self.enable_activation_key_placeholder(False) keys = entered_text.split(',') # keys == None clears keys in the module, so deleting keys # in the keys field will also clear module data on apply() self.subscription_request.activation_keys.set_secret(keys) self._update_registration_state() # system purpose related signals def on_system_purpose_checkbox_toggled(self, checkbox): active = checkbox.get_active() self._system_purpose_revealer.set_reveal_child(active) if active: # make sure data in the system purpose comboboxes # are forwarded to the system purpose data structure # in case something was set before they were hidden self.on_system_purpose_role_combobox_changed( self._system_purpose_role_combobox) self.on_system_purpose_sla_combobox_changed( self._system_purpose_sla_combobox) self.on_system_purpose_usage_combobox_changed( self._system_purpose_usage_combobox) else: # system purpose combo boxes have been hidden, clear the corresponding # data from the system purpose data structure, but keep it in the combo boxes # in case the user tries to show them again before next spoke entry clears them self.system_purpose_data.role = "" self.system_purpose_data.sla = "" self.system_purpose_data.usage = "" def on_system_purpose_role_combobox_changed(self, combobox): self.system_purpose_data.role = combobox.get_active_id() def on_system_purpose_sla_combobox_changed(self, combobox): self.system_purpose_data.sla = combobox.get_active_id() def on_system_purpose_usage_combobox_changed(self, combobox): self.system_purpose_data.usage = combobox.get_active_id() # HTTP proxy signals def on_http_proxy_checkbox_toggled(self, checkbox): active = checkbox.get_active() self._http_proxy_revealer.set_reveal_child(active) if active: # make sure data in the HTTP proxy entries # are forwarded to the subscription request structure # in case something was entered before they were hidden self.on_http_proxy_location_entry_changed( self._http_proxy_location_entry) self.on_http_proxy_username_entry_changed( self._http_proxy_username_entry) self.on_http_proxy_password_entry_changed( self._http_proxy_password_entry) else: # HTTP proxy entries have been hidden, clear the corresponding data from # the subscription request structure, but keep it in the entries in case # the user tries to show them again before next spoke entry clears them self._subscription_request.server_proxy_hostname = "" self._subscription_request.server_proxy_port = -1 self._subscription_request.server_proxy_user = "" self._subscription_request.server_proxy_password.set_secret(None) def on_http_proxy_location_entry_changed(self, editable): # Incorrect hostnames, including empty strings, will # throw an exception we need to catch and switch # to defaults. This can happen often as the user # types the hostname to the field. try: port = -1 # not set == -1 proxy_obj = ProxyString(url=editable.get_text()) hostname = proxy_obj.host if proxy_obj.port: # the DBus API expects an integer port = int(proxy_obj.port) except ProxyStringError: hostname = "" # set the resulting values to the DBus structure self.subscription_request.server_proxy_hostname = hostname self.subscription_request.server_proxy_port = port def on_http_proxy_username_entry_changed(self, editable): self.subscription_request.server_proxy_user = editable.get_text() def on_http_proxy_password_entry_changed(self, editable): password = editable.get_text() # if password is set in the field, set it, or set None to clear the password self.subscription_request.server_proxy_password.set_secret(password or None) # custom server hostname and rhsm baseurl signals def on_custom_server_hostname_checkbox_toggled(self, checkbox): active = checkbox.get_active() self._custom_server_hostname_revealer.set_reveal_child(active) if active: # make sure data in the server hostname entry # is forwarded to the subscription request structure # in case something was entered before the entry was # hidden self.on_custom_server_hostname_entry_changed( self._custom_server_hostname_entry) else: # the entry was hidden, clear the data from subscription request but # keep it in the entry in case user decides to show the entry again # before next spoke entry clears it self.subscription_request.server_hostname = "" def on_custom_server_hostname_entry_changed(self, editable): self.subscription_request.server_hostname = editable.get_text() def on_custom_rhsm_baseurl_checkbox_toggled(self, checkbox): active = checkbox.get_active() self._custom_rhsm_baseurl_revealer.set_reveal_child(active) if active: # make sure data in the rhsm baseurl entry # is forwarded to the subscription request structure # in case something was entered before the entry was # hidden self.on_custom_rhsm_baseurl_entry_changed( self._custom_rhsm_baseurl_entry) else: # the entry was hidden, clear the data from subscription request but # keep it in the entry in case user decides to show the entry again # before next spoke entry clears it self.subscription_request.rhsm_baseurl = "" def on_custom_rhsm_baseurl_entry_changed(self, editable): self.subscription_request.rhsm_baseurl = editable.get_text() # button signals def on_register_button_clicked(self, button): log.debug("Subscription GUI: register button clicked") self._register() def on_unregister_button_clicked(self, button): """Handle registration related tasks.""" log.debug("Subscription GUI: unregister button clicked") self._unregister() # properties - general properties @property def registration_phase(self): """Reports what phase the registration procedure is in. Only valid if a registration thread is running. """ return self._registration_phase @registration_phase.setter def registration_phase(self, phase): self._registration_phase = phase @property def subscription_attached(self): """Was a subscription entitlement successfully attached ?""" return self._subscription_module.IsSubscriptionAttached @property def network_connected(self): """Does it look like that we have network connectivity ? Network connectivity is required for subscribing a system. """ return self._network_module.Connected @property def authentication_method(self): """Report which authentication method is in use.""" return self._authentication_method @authentication_method.setter def authentication_method(self, method): self._authentication_method = method if method == AuthenticationMethod.USERNAME_PASSWORD: self.set_activation_key_visible(False) self.set_account_visible(True) self.subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD elif method == AuthenticationMethod.ORG_KEY: self.set_activation_key_visible(True) self.set_account_visible(False) self.subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY @property def options_set(self): """Report if at least one option in the Options section has been set.""" return self.http_proxy_visible or self.custom_server_hostname_visible or \ self.custom_rhsm_baseurl_visible @property def registration_error(self): return self._registration_error @registration_error.setter def registration_error(self, error_message): self._registration_error = error_message # also set the spoke warning banner self.show_warning_message(error_message) def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # get object references from the builders self._main_notebook = self.builder.get_object("main_notebook") # * the registration tab * # # container for the main registration controls self._registration_grid = self.builder.get_object("registration_grid") # authentication self._account_radio_button = self.builder.get_object( "account_radio_button") self._activation_key_radio_button = self.builder.get_object( "activation_key_radio_button") # authentication - account self._account_revealer = self.builder.get_object("account_revealer") self._username_entry = self.builder.get_object("username_entry") self._password_entry = self.builder.get_object("password_entry") # authentication - activation key self._activation_key_revealer = self.builder.get_object( "activation_key_revealer") self._organization_entry = self.builder.get_object( "organization_entry") self._activation_key_entry = self.builder.get_object( "activation_key_entry") # system purpose self._system_purpose_checkbox = self.builder.get_object( "system_purpose_checkbox") self._system_purpose_revealer = self.builder.get_object( "system_purpose_revealer") self._system_purpose_role_combobox = self.builder.get_object( "system_purpose_role_combobox") self._system_purpose_sla_combobox = self.builder.get_object( "system_purpose_sla_combobox") self._system_purpose_usage_combobox = self.builder.get_object( "system_purpose_usage_combobox") # insights self._insights_checkbox = self.builder.get_object("insights_checkbox") # options expander self._options_expander = self.builder.get_object("options_expander") # HTTP proxy self._http_proxy_checkbox = self.builder.get_object( "http_proxy_checkbox") self._http_proxy_revealer = self.builder.get_object( "http_proxy_revealer") self._http_proxy_location_entry = self.builder.get_object( "http_proxy_location_entry") self._http_proxy_username_entry = self.builder.get_object( "http_proxy_username_entry") self._http_proxy_password_entry = self.builder.get_object( "http_proxy_password_entry") # RHSM baseurl self._custom_rhsm_baseurl_checkbox = self.builder.get_object( "custom_rhsm_baseurl_checkbox") self._custom_rhsm_baseurl_revealer = self.builder.get_object( "custom_rhsm_baseurl_revealer") self._custom_rhsm_baseurl_entry = self.builder.get_object( "custom_rhsm_baseurl_entry") # server hostname self._custom_server_hostname_checkbox = self.builder.get_object( "custom_server_hostname_checkbox") self._custom_server_hostname_revealer = self.builder.get_object( "custom_server_hostname_revealer") self._custom_server_hostname_entry = self.builder.get_object( "custom_server_hostname_entry") # status label self._registration_status_label = self.builder.get_object( "registration_status_label") # register button self._register_button = self.builder.get_object("register_button") # * the subscription status tab * # # general status 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") self._usage_status_label = self.builder.get_object( "usage_status_label") self._insights_status_label = self.builder.get_object( "insights_status_label") # attached subscriptions self._attached_subscriptions_label = self.builder.get_object( "attached_subscriptions_label") self._subscriptions_listbox = self.builder.get_object( "subscriptions_listbox") # setup spoke state based on data from the Subscription DBus module self._update_spoke_state() # start the rest of spoke initialization which might take some time # (mainly due to waiting for various initialization threads to finish) # in a separate thread threadMgr.add( AnacondaThread(name=THREAD_SUBSCRIPTION_SPOKE_INIT, target=self._initialize)) def _initialize(self): # wait for subscription thread to finish (if any) threadMgr.wait(THREAD_SUBSCRIPTION) # also wait for the payload thread, which migh still be processing # a CDROM source, to avoid the Subscription being mandatory by mistake # due to CDN still being default at the time of evaulation threadMgr.wait(THREAD_PAYLOAD) # update overall state self._update_registration_state() self._update_subscription_state() # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__) # report that we are done self.initialize_done() # private methods def _update_spoke_state(self): """Setup spoke state based on Subscription DBus module state. Subscription DBus module state is represented by the SubscriptionRequest and SystemPurposeData DBus structures. We first update their local mirrors from the DBus module and then set all the controls in the spoke to values represented in the DBus structures. NOTE: There are a couple special cases where we need to do some special precessing, such as for fields holding sensitive data. If we blindly set those based on DBus structure data, we would effectively clear them as the Subscription DBus module never returns previously set sensitive data in plain text. """ # start by pulling in fresh data from the Subscription DBus module self._subscription_request = self._get_subscription_request() self._system_purpose_data = self._get_system_purpose_data() # next update the authentication part of the UI self._update_authetication_ui() # check if system purpose part of the spoke should be visible self.set_system_purpose_visible( self.system_purpose_data.check_data_available()) # NOTE: the fill_combobox() function makes sure to remove old data from the # combo box before filling it # role fill_combobox(self._system_purpose_role_combobox, self.system_purpose_data.role, self._subscription_module.GetValidRoles()) # SLA fill_combobox(self._system_purpose_sla_combobox, self.system_purpose_data.sla, self._subscription_module.GetValidSLAs()) # usage fill_combobox(self._system_purpose_usage_combobox, self.system_purpose_data.usage, self._subscription_module.GetValidUsageTypes()) # Insights self._insights_checkbox.set_active( self._subscription_module.InsightsEnabled) # update the HTTP proxy part of the UI self._update_http_proxy_ui() # set custom server hostname self.custom_server_hostname_visible = bool( self.subscription_request.server_hostname) self._custom_server_hostname_entry.set_text( self.subscription_request.server_hostname) # set custom rhsm baseurl self.custom_rhsm_baseurl_visible = bool( self.subscription_request.rhsm_baseurl) self._custom_rhsm_baseurl_entry.set_text( self.subscription_request.rhsm_baseurl) # if there is something set in the Options section, expand the expander # - this needs to go last, after all the values in option section are set/not set if self.options_set: self.set_options_visible(True) # now that we updated the spoke with fresh data from the module, we can run the # general purpose update functions that make sure the two parts of the spoke # (the registration part and the subscription part) are both valid self._update_registration_state() self._update_subscription_state() def _update_authetication_ui(self): """Update the authentication part of the spoke. - SubscriptionRequest always has type set - username + password is the default For the related password and activation keys entry holding sensitive data we need to reconcile the data held in the spoke from previous entry with data set in the DBus module previously: - data in module and entry empty -> set placeholder - data in module and entry populated -> keep text in entry, we assume it is the same as what is in module - no data in module and entry populated -> clear entry & any placeholders (data cleared over DBus API) - no data in module and entry empty -> do nothing """ if self.subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD: self.authentication_method = AuthenticationMethod.USERNAME_PASSWORD self._username_entry.set_text( self.subscription_request.account_username) set_in_entry = bool(self._password_entry.get_text()) set_in_module = self.subscription_request.account_password.type == SECRET_TYPE_HIDDEN if set_in_module: if not set_in_entry: self.enable_password_placeholder(True) else: self._password_entry.set_text("") self.enable_password_placeholder(False) elif self.subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_ORG_KEY: self.authentication_method = AuthenticationMethod.ORG_KEY self._organization_entry.set_text( self.subscription_request.organization) set_in_entry = bool(self._activation_key_entry.get_text()) set_in_module = self.subscription_request.activation_keys.type == SECRET_TYPE_HIDDEN if set_in_module: if not set_in_entry: self.enable_activation_key_placeholder(True) else: self._activation_key_entry.set_text("") self.enable_activation_key_placeholder(False) def _update_http_proxy_ui(self): """Update the HTTP proxy configuration part of the spoke.""" proxy_hostname = self.subscription_request.server_proxy_hostname proxy_port = self.subscription_request.server_proxy_port proxy_port_set = proxy_port >= 0 proxy_username = self.subscription_request.server_proxy_user proxy_password_secret = self.subscription_request.server_proxy_password proxy_password_set = proxy_password_secret.type == SECRET_TYPE_HIDDEN self.http_proxy_visible = proxy_hostname or proxy_username or proxy_password_set if proxy_hostname: proxy_url = proxy_hostname if proxy_port_set: proxy_url = "{}:{}".format(proxy_url, proxy_port) self._http_proxy_location_entry.set_text(proxy_url) # HTTP proxy username self._http_proxy_username_entry.set_text(proxy_username) # HTTP proxy password set_in_entry = bool(self._http_proxy_password_entry.get_text()) secret_type = self.subscription_request.server_proxy_password.type set_in_module = secret_type == SECRET_TYPE_HIDDEN if set_in_module: if not set_in_entry: self.enable_http_proxy_password_placeholder(True) else: self._http_proxy_password_entry.set_text("") self.enable_http_proxy_password_placeholder(False) def _set_data_to_module(self): """Set system purpose data to the DBus module. Called either on apply() or right before a subscription attempt. """ self._set_system_purpose_data() # Set data about Insights to the DBus module. self._set_insights() # Set subscription request to the DBus module. self._set_subscription_request() def _get_system_purpose_data(self): """Get SystemPurposeData from the Subscription module.""" struct = self._subscription_module.SystemPurposeData return SystemPurposeData.from_structure(struct) def _set_system_purpose_data(self): """Set system purpose data to the Subscription DBus module.""" self._subscription_module.SetSystemPurposeData( SystemPurposeData.to_structure(self.system_purpose_data)) # also apply the data (only applies when needed) self._apply_system_purpose_data() def _apply_system_purpose_data(self): """Apply system purpose data to the installation environment. Apply system purpose data to the installation environment, provided that: - system purpose data has not yet been applied to the system or - current system purpose data is different from the data last applied to the system Due to that we keep a copy of the last applied system purpose data so that we can check for difference. If the last applied data is the same as current system purpose data, nothing is done. """ if self._last_applied_system_purpose_data != self.system_purpose_data: log.debug( "Subscription GUI: applying system purpose data to installation environment" ) task_path = self._subscription_module.SetSystemPurposeWithTask() task_proxy = SUBSCRIPTION.get_proxy(task_path) sync_run_task(task_proxy) self._last_applied_system_purpose_data = self.system_purpose_data def _get_subscription_request(self): """Get SubscriptionRequest from the Subscription module.""" struct = self._subscription_module.SubscriptionRequest return SubscriptionRequest.from_structure(struct) def _set_subscription_request(self): """Set subscription request to the Subscription DBus module.""" self._subscription_module.SetSubscriptionRequest( SubscriptionRequest.to_structure(self.subscription_request)) def _set_insights(self): """Configure Insights in DBus module based on GUI state.""" self._subscription_module.SetInsightsEnabled( self._insights_checkbox.get_active()) def _register(self): """Try to register a system.""" # update data in the Subscription DBUS module self._set_data_to_module() # disable controls self.set_registration_controls_sensitive(False) # wait for the previous subscription thread to finish threadMgr.wait(THREAD_SUBSCRIPTION) # check if the current installation source will be overriden # and remember it if it is the case source_proxy = self.payload.get_source_proxy() source_type = source_proxy.Type if source_type in SOURCE_TYPES_OVERRIDEN_BY_CDN: self._overridden_source_type = source_type else: # no override will happen, so clear the variable self._overridden_source_type = None # try to register log.debug("Subscription GUI: attempting to register") threadMgr.add( AnacondaThread(name=THREAD_SUBSCRIPTION, target=register_and_subscribe, kwargs={ "payload": self.payload, "progress_callback": self._subscription_progress_callback, "error_callback": self._subscription_error_callback, "restart_payload": True })) def _unregister(self): """Try to unregister a system.""" # update data in the Subscription DBUS module self._set_data_to_module() # disable controls self.set_registration_controls_sensitive(False) # wait for the previous subscription thread to finish threadMgr.wait(THREAD_SUBSCRIPTION) # try to unregister log.debug("Subscription GUI: attempting to unregister") threadMgr.add( AnacondaThread(name=THREAD_SUBSCRIPTION, target=unregister, kwargs={ "payload": self.payload, "overridden_source_type": self._overridden_source_type, "progress_callback": self._subscription_progress_callback, "error_callback": self._subscription_error_callback, "restart_payload": True })) @async_action_wait def _subscription_progress_callback(self, phase): """Progress handling for subscription thread. Used both for both registration + attaching subscription and for unregistration. NOTE: Using the @async_action_wait decorator as this is called from the subscription thread. We need to do that as GTK does bad things if non main threads interact with it. """ # clear error message from a previous attempt (if any) self.registration_error = "" # set registration phase self.registration_phase = phase # set spoke status according to subscription thread phase if phase == SubscriptionPhase.DONE: log.debug("Subscription GUI: registration & attach done") # we are done, clear the phase self.registration_phase = None # update registration and subscription parts of the spoke self._update_registration_state() self._update_subscription_state() # enable controls self.set_registration_controls_sensitive(True) # notify hub hubQ.send_ready(self.__class__.__name__) else: # processing still ongoing, set the phase self.registration_phase = phase # notify hub hubQ.send_ready(self.__class__.__name__) # update spoke state self._update_registration_state() @async_action_wait def _subscription_error_callback(self, error_message): log.debug("Subscription GUI: registration & attach failed") # store the error message self.registration_error = error_message # 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() # re-enable controls, so user can try again self.set_registration_controls_sensitive(True) # notify hub hubQ.send_ready(self.__class__.__name__) def _get_status_message(self): """Get status message describing current spoke state. The registration phase is taken into account (if any) as well as possible error state and subscription being or not being attached. NOTE: This method is used both for the spoke status message as well as for the in-spoke status label. """ phase = self.registration_phase if phase: if phase == SubscriptionPhase.UNREGISTER: return _("Unregistering...") elif phase == SubscriptionPhase.REGISTER: return _("Registering...") elif phase == SubscriptionPhase.ATTACH_SUBSCRIPTION: return _("Attaching subscription...") elif phase == SubscriptionPhase.DONE: return _("Subscription attached.") elif self.registration_error: return _("Registration failed.") elif self.subscription_attached: return _("Registered.") else: return _("Not registered.") @async_action_wait def _update_registration_state(self): """Update state of the registration related part of the spoke. Hopefully this method is not too inefficient as it is running basically on every keystroke in the username/password/organization/key entry. """ subscription_attached = self.subscription_attached if subscription_attached: self._main_notebook.set_current_page(self.SUBSCRIPTION_STATUS_PAGE) else: self._main_notebook.set_current_page(self.REGISTRATION_PAGE) # update registration status label self._registration_status_label.set_text(self._get_status_message()) # update registration button state self._update_register_button_state() @async_action_wait def _update_subscription_state(self): """Update state of the subscription related part of the spoke. Update state of the part of the spoke, that shows data about the currently attached subscriptions. """ # authentication method if self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD: method_string = _("Registered with account {}").format( self.subscription_request.account_username) else: # org + key method_string = _("Registered with organization {}").format( self.subscription_request.organization) self._method_status_label.set_text(method_string) # final syspurpose data # role final_role_string = _("Role: {}").format(self.system_purpose_data.role) self._role_status_label.set_text(final_role_string) # SLA final_sla_string = _("SLA: {}").format(self.system_purpose_data.sla) self._sla_status_label.set_text(final_sla_string) # usage final_usage_string = _("Usage: {}").format( self.system_purpose_data.usage) self._usage_status_label.set_text(final_usage_string) # Insights # - this strings are referring to the desired target system state, # the installation environment itself is not expected to be # connected to Insights if self._subscription_module.InsightsEnabled: insights_string = _("Connected to Red Hat Insights") else: 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) 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. Network connectivity is required for using the Subscription spoke for obvious reasons (eq. for communication with the remote Candlepin instance & CDN). If network is already available, this method makes the registration controls sensitive and clears any previous connectivity warnings. If network is not available it makes the registration controls insensitive and displays a warning to the user. """ network_connected = self.network_connected if network_connected: # make controls sensitive, unless processing is ongoing self.set_registration_controls_sensitive(True) if not self._network_connected_previously: # clear previous connectivity warning # - we only do this on connectivity state change so that we don't clear # registration error related warnings log.debug("Subscription GUI: clearing connectivity warning") self.clear_info() else: # make controls insensitive self.set_registration_controls_sensitive(False) # set a warning log.debug("Subscription GUI: setting connectivity warning") self.show_warning_message( _("Please enable network access before connecting to Red Hat.") ) # remember state self._network_connected_previously = network_connected def _update_register_button_state(self): """Update register button state. The button is only sensitive if no processing is ongoing and we either have enough authentication data to register or the system is subscribed, so we can unregister it. """ button_sensitive = False if self._registration_controls_enabled: # if we are subscribed, we can always unregister if self.subscription_attached: button_sensitive = True # check if credentials are sufficient for registration elif self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD: button_sensitive = username_password_sufficient( self.subscription_request) elif self.authentication_method == AuthenticationMethod.ORG_KEY: button_sensitive = org_keys_sufficient( self.subscription_request) self._register_button.set_sensitive(button_sensitive)
class SoftwareSelectionSpoke(NormalSpoke): """ .. inheritance-diagram:: SoftwareSelectionSpoke :parts: 3 """ builderObjects = ["addonStore", "environmentStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software_selection.glade" helpFile = "SoftwareSpoke.xml" category = SoftwareCategory icon = "package-x-generic-symbolic" title = CN_("GUI|Spoke", "_Software Selection") @classmethod def should_run(cls, environment, data): """Don't run for any non-package payload.""" if not NormalSpoke.should_run(environment, data): return False return context.payload_type == PAYLOAD_TYPE_DNF def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._errors = [] self._warnings = [] self._tx_id = None # Get the packages selection data. self._selection_cache = SoftwareSelectionCache(self._dnf_manager) self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted # Get the UI elements. self._environment_list_box = self.builder.get_object("environmentListBox") self._addon_list_box = self.builder.get_object("addonListBox") # Connect viewport scrolling with listbox focus events environment_viewport = self.builder.get_object("environmentViewport") self._environment_list_box.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(environment_viewport) ) addon_viewport = self.builder.get_object("addonViewport") self._addon_list_box.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(addon_viewport) ) @property def _dnf_manager(self): """The DNF manager.""" return self.payload.dnf_manager @property def _selection(self): """The packages selection.""" return self.payload.get_packages_selection() def initialize(self): """Initialize the spoke.""" super().initialize() self.initialize_start() threadMgr.add(AnacondaThread( name=THREAD_SOFTWARE_WATCHER, target=self._initialize )) def _initialize(self): """Initialize the spoke in a separate thread.""" threadMgr.wait(THREAD_PAYLOAD) # Initialize and check the software selection. self._initialize_selection() # Update the status. hubQ.send_ready(self.__class__.__name__) # Report that the software spoke has been initialized. self.initialize_done() def _initialize_selection(self): """Initialize and check the software selection.""" if not self.payload.base_repo: log.debug("Skip the initialization of the software selection.") return if not self._kickstarted: # Use the default environment. self._selection_cache.select_environment( self._dnf_manager.default_environment ) # Apply the default selection. self.apply() # Check the initial software selection. self.execute() # Wait for the software selection thread that might be started by execute(). # We are already running in a thread, so it should not needlessly block anything # and only like this we can be sure we are really initialized. threadMgr.wait(THREAD_CHECK_SOFTWARE) @property def ready(self): """Is the spoke ready? By default, the software selection spoke is not ready. We have to wait until the installation source spoke is completed. This could be because the user filled something out, or because we're done fetching repo metadata from the mirror list, or we detected a DVD/CD. """ return not self._processing_data and self._source_is_set @property def _source_is_set(self): """Is the installation source set?""" return self.payload.base_repo is not None @property def _source_has_changed(self): """Has the installation source changed?""" return self._tx_id != self.payload.tx_id @property def _processing_data(self): """Is the spoke processing data?""" return threadMgr.get(THREAD_SOFTWARE_WATCHER) \ or threadMgr.get(THREAD_PAYLOAD) \ or threadMgr.get(THREAD_CHECK_SOFTWARE) @property def status(self): """The status of the spoke.""" if self._processing_data: return _("Processing...") if is_cdn_registration_required(self.payload): return _("Red Hat CDN requires registration.") if not self._source_is_set: return _("Installation source not set up") if self._source_has_changed: return _("Source changed - please verify") if self._errors: return _("Error checking software selection") if self._warnings: return _("Warning checking software selection") return get_software_selection_status( dnf_manager=self._dnf_manager, selection=self._selection, kickstarted=self._kickstarted ) @property def completed(self): """Is the spoke complete?""" return self.ready \ and not self._errors \ and not self._source_has_changed \ and is_software_selection_complete( dnf_manager=self._dnf_manager, selection=self._selection, kickstarted=self._kickstarted ) def refresh(self): super().refresh() threadMgr.wait(THREAD_PAYLOAD) # Create a new software selection cache. self._selection_cache = SoftwareSelectionCache(self._dnf_manager) self._selection_cache.apply_selection_data(self._selection) # Refresh up the UI. self._refresh_environments() self._refresh_groups() # Set up the info bar. self.clear_info() if self._errors: self.set_warning(_( "Error checking software dependencies. " " <a href=\"\">Click for details.</a>" )) elif self._warnings: self.set_warning(_( "Warning checking software dependencies. " " <a href=\"\">Click for details.</a>" )) def _refresh_environments(self): """Create rows for all available environments.""" self._clear_listbox(self._environment_list_box) for environment in self._selection_cache.available_environments: # Get the environment data. data = self._dnf_manager.get_environment_data(environment) selected = self._selection_cache.is_environment_selected(environment) # Add a new environment row. row = EnvironmentListBoxRow(data, selected) self._environment_list_box.insert(row, -1) self._environment_list_box.show_all() def _refresh_groups(self): """Create rows for all available groups.""" self._clear_listbox(self._addon_list_box) if self._selection_cache.environment: # Get the environment data. environment_data = self._dnf_manager.get_environment_data( self._selection_cache.environment ) # Add all optional groups. for group in environment_data.optional_groups: self._add_group_row(group) # Add the separator. if environment_data.optional_groups and environment_data.visible_groups: self._addon_list_box.insert(SeparatorRow(), -1) # Add user visible groups that are not optional. for group in environment_data.visible_groups: if group in environment_data.optional_groups: continue self._add_group_row(group) self._addon_list_box.show_all() def _add_group_row(self, group): """Add a new row for the specified group.""" # Get the group data. data = self._dnf_manager.get_group_data(group) selected = self._selection_cache.is_group_selected(group) # Add a new group row. row = GroupListBoxRow(data, selected) self._addon_list_box.insert(row, -1) def _clear_listbox(self, listbox): for child in listbox.get_children(): listbox.remove(child) del child def apply(self): """Apply the changes.""" self._kickstarted = False selection = self._selection_cache.get_selection_data() log.debug("Setting new software selection: %s", selection) self.payload.set_packages_selection(selection) hubQ.send_not_ready(self.__class__.__name__) hubQ.send_not_ready("SourceSpoke") def execute(self): """Execute the changes.""" threadMgr.add(AnacondaThread( name=THREAD_CHECK_SOFTWARE, target=self._check_software_selection )) def _check_software_selection(self): hubQ.send_message(self.__class__.__name__, _("Checking software dependencies...")) self.payload.bump_tx_id() # Run the validation task. from pyanaconda.modules.payloads.payload.dnf.validation import CheckPackagesSelectionTask task = CheckPackagesSelectionTask( dnf_manager=self._dnf_manager, selection=self._selection, ) # Get the validation report. report = task.run() log.debug("The selection has been checked: %s", report) self._errors = list(report.error_messages) self._warnings = list(report.warning_messages) self._tx_id = self.payload.tx_id hubQ.send_ready(self.__class__.__name__) hubQ.send_ready("SourceSpoke") # Signal handlers def on_environment_activated(self, listbox, row): if not isinstance(row, EnvironmentListBoxRow): return # Mark the environment as selected. environment = row.get_environment_id() self._selection_cache.select_environment(environment) # Update the row button. row.toggle_button(True) # Update the screen. self._refresh_groups() def on_addon_activated(self, listbox, row): # Skip the separator. if not isinstance(row, GroupListBoxRow): return # Mark the group as selected or deselected. group = row.get_group_id() selected = not self._selection_cache.is_group_selected(group) if selected: self._selection_cache.select_group(row.data.id) else: self._selection_cache.deselect_group(row.data.id) # Update the row button. row.toggle_button(selected) def on_info_bar_clicked(self, *args): if self._errors: self._show_error_dialog() elif self._warnings: self._show_warning_dialog() def _show_error_dialog(self): label = _( "The software marked for installation has the following errors. " "This is likely caused by an error with your installation source. " "You can quit the installer, change your software source, or change " "your software selections." ) buttons = [ C_("GUI|Software Selection|Error Dialog", "_Quit"), C_("GUI|Software Selection|Error Dialog", "_Modify Software Source"), C_("GUI|Software Selection|Error Dialog", "Modify _Selections") ] dialog = DetailedErrorDialog(self.data, buttons=buttons, label=label) with self.main_window.enlightbox(dialog.window): errors = "\n".join(self._errors) dialog.refresh(errors) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit the installation. ipmi_abort(scripts=self.data.scripts) sys.exit(0) elif rc == 1: # Send the user to the installation source spoke. self.skipTo = "SourceSpoke" self.window.emit("button-clicked") def _show_warning_dialog(self): label = _( "The software marked for installation has the following warnings. " "These are not fatal, but you may wish to make changes to your " "software selections." ) buttons = [ C_("GUI|Software Selection|Warning Dialog", "_OK") ] dialog = DetailedErrorDialog(self.data, buttons=buttons, label=label) with self.main_window.enlightbox(dialog.window): warnings = "\n".join(self._warnings) dialog.refresh(warnings) dialog.run() dialog.window.destroy()
class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ builderObjects = ["userCreationWindow"] mainWidgetName = "userCreationWindow" focusWidgetName = "fullname_entry" uiFile = "spokes/user.glade" helpFile = "UserSpoke.xml" category = UserSettingsCategory icon = "avatar-default-symbolic" title = CN_("GUI|Spoke", "_User Creation") @classmethod def should_run(cls, environment, data): # the user spoke should run always in the anaconda and in firstboot only # when doing reconfig or if no user has been created in the installation if environment == constants.ANACONDA_ENVIRON: return True elif environment == constants.FIRSTBOOT_ENVIRON and data is None: # cannot decide, stay in the game and let another call with data # available (will come) decide return True elif environment == constants.FIRSTBOOT_ENVIRON and data and len( data.user.userList) == 0: return True else: return False def __init__(self, *args): NormalSpoke.__init__(self, *args) GUISpokeInputCheckHandler.__init__(self) self._users_module = USERS.get_observer() self._users_module.connect() def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # Create a new UserData object to store this spoke's state # as well as the state of the advanced user dialog. if self.data.user.userList: self._user = copy.copy(self.data.user.userList[0]) else: self._user = self.data.UserData() # gather references to relevant GUI objects # entry fields self._fullname_entry = self.builder.get_object("fullname_entry") self._username_entry = self.builder.get_object("username_entry") self._password_entry = self.builder.get_object("password_entry") self._password_confirmation_entry = self.builder.get_object( "password_confirmation_entry") # check boxes self._admin_checkbox = self.builder.get_object("admin_checkbox") self._password_required_checkbox = self.builder.get_object( "password_required_checkbox") # advanced user configration dialog button self._advanced_button = self.builder.get_object("advanced_button") # password checking status bar & label self._password_bar = self.builder.get_object("password_bar") self._password_label = self.builder.get_object("password_label") # Install the password checks: # - Has a password been specified? # - If a password has been specified and there is data in the confirm box, do they match? # - How strong is the password? # - Does the password contain non-ASCII characters? # Setup the password checker for password checking self._checker = input_checking.PasswordChecker( initial_password_content=self.password, initial_password_confirmation_content=self.password_confirmation, policy=input_checking.get_policy(self.data, "user")) # configure the checker for password checking self.checker.username = self.username self.checker.secret_type = constants.SecretType.PASSWORD # remove any placeholder texts if either password or confirmation field changes content from initial state self.checker.password.changed_from_initial_state.connect( self.remove_placeholder_texts) self.checker.password_confirmation.changed_from_initial_state.connect( self.remove_placeholder_texts) # connect UI updates to check results self.checker.checks_done.connect(self._checks_done) # username and full name checks self._username_check = input_checking.UsernameCheck() self._fullname_check = input_checking.FullnameCheck() # empty username is considered a success so that the user can leave # the spoke without filling it in self._username_check.success_if_username_empty = True # check that the password is not empty self._empty_check = input_checking.PasswordEmptyCheck() # check that the content of the password field & the conformation field are the same self._confirm_check = input_checking.PasswordConfirmationCheck() # check password validity, quality and strength self._validity_check = input_checking.PasswordValidityCheck() # connect UI updates to validity check results self._validity_check.result.password_score_changed.connect( self.set_password_score) self._validity_check.result.status_text_changed.connect( self.set_password_status) # check if the password contains non-ascii characters self._ascii_check = input_checking.PasswordASCIICheck() # register the individual checks with the checker in proper order # 0) is the username and fullname valid ? # 1) is the password non-empty ? # 2) are both entered passwords the same ? # 3) is the password valid according to the current password checking policy ? # 4) is the password free of non-ASCII characters ? self.checker.add_check(self._username_check) self.checker.add_check(self._fullname_check) self.checker.add_check(self._empty_check) self.checker.add_check(self._confirm_check) self.checker.add_check(self._validity_check) self.checker.add_check(self._ascii_check) self.guesser = {self.username_entry: True} # Configure levels for the password bar self.password_bar.add_offset_value("low", 2) self.password_bar.add_offset_value("medium", 3) self.password_bar.add_offset_value("high", 4) # indicate when the password was set by kickstart self.password_kickstarted = self.data.user.seen # Modify the GUI based on the kickstart and policy information # This needs to happen after the input checks have been created, since # the Gtk signal handlers use the input check variables. password_set_message = _("The password was set by kickstart.") if self.password_kickstarted: self.password_required = True self.password_entry.set_placeholder_text(password_set_message) self.password_confirmation_entry.set_placeholder_text( password_set_message) elif not self.checker.policy.emptyok: # Policy is that a non-empty password is required self.password_required = True if not self.checker.policy.emptyok: # User isn't allowed to change whether password is required or not self.password_required_checkbox.set_sensitive(False) self._advanced_user_dialog = AdvancedUserDialog(self._user, self.data) self._advanced_user_dialog.initialize() # set the visibility of the password entries set_password_visibility(self.password_entry, False) set_password_visibility(self.password_confirmation_entry, False) # report that we are done self.initialize_done() @property def username_entry(self): return self._username_entry @property def username(self): return self.username_entry.get_text() @username.setter def username(self, new_username): self.username_entry.set_text(new_username) @property def fullname_entry(self): return self._fullname_entry @property def fullname(self): return self.fullname_entry.get_text() @fullname.setter def fullname(self, new_fullname): self.fullname_entry.set_text(new_fullname) @property def password_required_checkbox(self): return self._password_required_checkbox @property def password_required(self): return self.password_required_checkbox.get_active() @password_required.setter def password_required(self, value): self.password_required_checkbox.set_active(value) def refresh(self): self.username = self._user.name self.fullname = self._user.gecos self._admin_checkbox.set_active("wheel" in self._user.groups) # rerun checks so that we have a correct status message, if any self.checker.run_checks() @property def status(self): if len(self.data.user.userList) == 0: return _("No user will be created") elif "wheel" in self.data.user.userList[0].groups: return _("Administrator %s will be created" ) % self.data.user.userList[0].name else: return _( "User %s will be created") % self.data.user.userList[0].name @property def mandatory(self): """ Only mandatory if the root pw hasn't been set in the UI eg. not mandatory if the root account was locked in a kickstart """ return not self._users_module.proxy.IsRootPasswordSet and not self._users_module.proxy.IsRootAccountLocked def apply(self): # set the password only if the user enters anything to the text entry # this should preserve the kickstart based password if self.password_required: if self.password: self.password_kickstarted = False self._user.password = cryptPassword(self.password) self._user.isCrypted = True self.remove_placeholder_texts() # reset the password when the user unselects it else: self.remove_placeholder_texts() self._user.password = "" self._user.isCrypted = False self.password_kickstarted = False self._user.name = self.username self._user.gecos = self.fullname # Copy the spoke data back to kickstart # If the user name is not set, no user will be created. if self._user.name: ksuser = copy.copy(self._user) if not self.data.user.userList: self.data.user.userList.append(ksuser) else: self.data.user.userList[0] = ksuser elif self.data.user.userList: self.data.user.userList.pop(0) @property def sensitive(self): # Spoke cannot be entered if a user was set in the kickstart and the user # policy doesn't allow changes. return not (self.completed and flags.automatedInstall and self.data.user.seen and not self.checker.policy.changesok) @property def completed(self): return len(self.data.user.userList) > 0 def password_required_toggled(self, togglebutton=None, data=None): """Called by Gtk callback when the "Use password" check button is toggled. It will make password entries in/sensitive.""" password_is_required = togglebutton.get_active() self.password_entry.set_sensitive(password_is_required) self.password_confirmation_entry.set_sensitive(password_is_required) # also disable/enable corresponding password checks self._empty_check.skip = not password_is_required self._confirm_check.skip = not password_is_required self._validity_check.skip = not password_is_required self._ascii_check.skip = not password_is_required # and rerun the checks self.checker.run_checks() def on_password_icon_clicked(self, entry, icon_pos, event): """Called by Gtk callback when the icon of a password entry is clicked.""" set_password_visibility(entry, not entry.get_visibility()) def on_username_set_by_user(self, editable, data=None): """Called by Gtk on user-driven changes to the username field. This handler is blocked during changes from the username guesser. """ # If the user set a user name, turn off the username guesser. # If the user cleared the username, turn it back on. if editable.get_text(): self.guesser = False else: self.guesser = True def on_username_changed(self, editable, data=None): """Called by Gtk on all username changes.""" new_username = editable.get_text() # Disable the advanced user dialog button when no username is set if editable.get_text(): self._advanced_button.set_sensitive(True) else: self._advanced_button.set_sensitive(False) # update the username in checker self.checker.username = new_username # Skip the empty password checks if no username is set, # otherwise the user will not be able to leave the # spoke if password is not set but policy requires that. self._empty_check.skip = not new_username self._validity_check.skip = not new_username # Re-run the password checks against the new username self.checker.run_checks() def on_full_name_changed(self, editable, data=None): """Called by Gtk callback when the full name field changes.""" fullname = editable.get_text() if self.guesser: username = guess_username(fullname) with blockedHandler(self.username_entry, self.on_username_set_by_user): self.username = username self.checker.fullname = fullname # rerun the checks self.checker.run_checks() def on_admin_toggled(self, togglebutton, data=None): # Add or remove "wheel" from the grouplist on changes to the admin checkbox if togglebutton.get_active(): if "wheel" not in self._user.groups: self._user.groups.append("wheel") elif "wheel" in self._user.groups: self._user.groups.remove("wheel") def on_advanced_clicked(self, _button, data=None): """Handler for the Advanced.. button. It starts the Advanced dialog for setting homedir, uid, gid and groups. """ self._user.name = self.username self._advanced_user_dialog.refresh() with self.main_window.enlightbox(self._advanced_user_dialog.window): self._advanced_user_dialog.run() self._admin_checkbox.set_active("wheel" in self._user.groups) def _checks_done(self, error_message): """Update the warning with the input validation error from the first error message or clear warnings if all the checks were successful. Also appends the "press twice" suffix if compatible with current password policy and handles the press-done-twice logic. """ # check if an unwaivable check failed unwaivable_checks = [ not self._confirm_check.result.success, not self._username_check.result.success, not self._fullname_check.result.success, not self._empty_check.result.success ] # with emptyok == False the empty password check become unwaivable #if not self.checker.policy.emptyok: # unwaivable_checks.append(not self._empty_check.result.success) unwaivable_check_failed = any(unwaivable_checks) # set appropriate status bar message if not error_message: # all is fine, just clear the message self.clear_info() elif not self.username and not self.password and not self.password_confirmation: # Clear any info message if username and both the password and password # confirmation fields are empty. # This shortcut is done to make it possible for the user to leave the spoke # without inputting any username or password. Separate logic makes sure an # empty string is not unexpectedly set as the user password. self.clear_info() elif not self.username and not self.password and not self.password_confirmation: # Also clear warnings if username is set but empty password is fine. self.clear_info() else: if self.checker.policy.strict or unwaivable_check_failed: # just forward the error message self.show_warning_message(error_message) else: # add suffix for the click twice logic self.show_warning_message("{} {}".format( error_message, _(constants.PASSWORD_DONE_TWICE))) # check if the spoke can be exited after the latest round of checks self._check_spoke_exit_conditions(unwaivable_check_failed) def _check_spoke_exit_conditions(self, unwaivable_check_failed): """Check if the user can escape from the root spoke or stay forever !""" # reset any waiving in progress self.waive_clicks = 0 # Depending on the policy we allow users to waive the password strength # and non-ASCII checks. If the policy is set to strict, the password # needs to be strong, but can still contain non-ASCII characters. self.can_go_back = False self.needs_waiver = True # This shortcut is done to make it possible for the user to leave the spoke # without inputting anything. Separate logic makes sure an # empty string is not unexpectedly set as the user password. if not self.username and not self.password and not self.password_confirmation: self.can_go_back = True self.needs_waiver = False elif self.checker.success: # if all checks were successful we can always go back to the hub self.can_go_back = True self.needs_waiver = False elif unwaivable_check_failed: self.can_go_back = False elif not self.password and not self.password_confirmation: self.can_go_back = True self.needs_waiver = False else: if self.checker.policy.strict: if not self._validity_check.result.success: # failing validity check in strict # mode prevents us from going back self.can_go_back = False elif not self._ascii_check.result.success: # but the ASCII check can still be waived self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False else: if not self._confirm_check.result.success: self.can_go_back = False if not self._validity_check.result.success: self.can_go_back = True self.needs_waiver = True elif not self._ascii_check.result.success: self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False def on_back_clicked(self, button): # the GUI spoke input check handler handles the spoke exit logic for us if self.try_to_go_back(): NormalSpoke.on_back_clicked(self, button) else: log.info("Return to hub prevented by password checking rules.")
class StorageSpoke(NormalSpoke, StorageCheckHandler): """ .. inheritance-diagram:: StorageSpoke :parts: 3 """ builderObjects = ["storageWindow", "addSpecializedImage"] mainWidgetName = "storageWindow" uiFile = "spokes/storage.glade" category = SystemCategory # other candidates: computer-symbolic, folder-symbolic icon = "drive-harddisk-symbolic" title = CN_("GUI|Spoke", "Installation _Destination") @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "storage-configuration" @classmethod def should_run(cls, environment, data): """Don't run the storage spoke on dir installations.""" if not NormalSpoke.should_run(environment, data): return False return not conf.target.is_directory def __init__(self, *args, **kwargs): StorageCheckHandler.__init__(self) NormalSpoke.__init__(self, *args, **kwargs) self.applyOnSkip = True self._ready = False self._back_clicked = False self._disks_errors = [] self._last_clicked_overview = None self._cur_clicked_overview = None self._storage_module = STORAGE.get_proxy() self._device_tree = STORAGE.get_proxy(DEVICE_TREE) self._bootloader_module = STORAGE.get_proxy(BOOTLOADER) self._disk_init_module = STORAGE.get_proxy(DISK_INITIALIZATION) self._disk_select_module = STORAGE.get_proxy(DISK_SELECTION) # This list contains all possible disks that can be included in the install. # All types of advanced disks should be set up for us ahead of time, so # there should be no need to modify this list. self._available_disks = [] self._selected_disks = [] self._last_selected_disks = [] # Is the partitioning already configured? self._is_preconfigured = bool(self._storage_module.CreatedPartitioning) # Find a partitioning to use. self._partitioning = find_partitioning() self._last_partitioning_method = self._partitioning.PartitioningMethod # Create a partitioning request for the automatic partitioning. self._partitioning_request = PartitioningRequest() if self._last_partitioning_method == PARTITIONING_METHOD_AUTOMATIC: self._partitioning_request = PartitioningRequest.from_structure( self._partitioning.Request) # Get the UI elements. self._custom_part_radio_button = self.builder.get_object( "customRadioButton") self._blivet_gui_radio_button = self.builder.get_object( "blivetguiRadioButton") self._encrypted_checkbox = self.builder.get_object( "encryptionCheckbox") self._encryption_revealer = self.builder.get_object( "encryption_revealer") self._reclaim_checkbox = self.builder.get_object("reclaimCheckbox") self._reclaim_revealer = self.builder.get_object( "reclaim_checkbox_revealer") self._local_disks_box = self.builder.get_object("local_disks_box") self._specialized_disks_box = self.builder.get_object( "specialized_disks_box") self._local_viewport = self.builder.get_object("localViewport") self._specialized_viewport = self.builder.get_object( "specializedViewport") self._main_viewport = self.builder.get_object("storageViewport") self._main_box = self.builder.get_object("storageMainBox") # Configure the partitioning methods. self._configure_partitioning_methods() def _configure_partitioning_methods(self): if "CustomPartitioningSpoke" in conf.ui.hidden_spokes: self._custom_part_radio_button.set_visible(False) self._custom_part_radio_button.set_no_show_all(True) if "BlivetGuiSpoke" in conf.ui.hidden_spokes or not self._is_blivet_gui_supported( ): self._blivet_gui_radio_button.set_visible(False) self._blivet_gui_radio_button.set_no_show_all(True) def _is_blivet_gui_supported(self): """Is the partitioning with blivet-gui supported?""" try: import pyanaconda.ui.gui.spokes.blivet_gui # pylint:disable=unused-import except ImportError: return False return True def _get_selected_partitioning_method(self): """Get the selected partitioning method. Return partitioning method according to which method selection radio button is currently active. """ if self._custom_part_radio_button.get_active(): return PARTITIONING_METHOD_INTERACTIVE if self._blivet_gui_radio_button.get_active(): return PARTITIONING_METHOD_BLIVET return PARTITIONING_METHOD_AUTOMATIC def on_method_toggled(self, radio_button): """Triggered when one of the partitioning method radio buttons is toggled.""" # Run only for a visible active radio button. if not radio_button.get_visible() or not radio_button.get_active(): return # Get the selected patitioning method. current_partitioning_method = self._get_selected_partitioning_method() # Hide the encryption checkbox for Blivet GUI storage configuration, # as Blivet GUI handles encryption per encrypted device, not globally. # Hide it also for the interactive partitioning as CustomPartitioningSpoke # provides support for encryption of mount points. self._encryption_revealer.set_reveal_child( current_partitioning_method == PARTITIONING_METHOD_AUTOMATIC) # Hide the reclaim space checkbox if automatic storage configuration is not used. self._reclaim_revealer.set_reveal_child( current_partitioning_method == PARTITIONING_METHOD_AUTOMATIC) # Is this a change from the last used method ? method_changed = current_partitioning_method != self._last_partitioning_method # Are there any actions planned ? if self._storage_module.AppliedPartitioning: if method_changed: # clear any existing messages from the info bar # - this generally means various storage related error warnings self.clear_info() self.set_warning( _("Partitioning method changed - planned storage configuration " "changes will be cancelled.")) else: self.clear_info() # reinstate any errors that should be shown to the user self._check_problems() def apply(self): self._disk_init_module.InitializationMode = CLEAR_PARTITIONS_NONE self._disk_init_module.InitializeLabelsEnabled = True apply_disk_selection(self._selected_disks, reset_boot_drive=True) @async_action_nowait def execute(self): """Apply a partitioning.""" # Make sure that we apply a non-interactive partitioning. if self._last_partitioning_method == PARTITIONING_METHOD_INTERACTIVE: log.debug( "Skipping the execute method for the INTERACTIVE partitioning method." ) return if self._last_partitioning_method == PARTITIONING_METHOD_BLIVET: log.debug( "Skipping the execute method for the BLIVET partitioning method." ) return log.debug("Running the execute method for the %s partitioning method.", self._last_partitioning_method) # Spawn storage execution as a separate thread so there's no big delay # going back from this spoke to the hub while StorageCheckHandler.run runs. # Yes, this means there's a thread spawning another thread. Sorry. threadMgr.add( AnacondaThread(name=constants.THREAD_EXECUTE_STORAGE, target=self._do_execute)) def _do_execute(self): """Apply a non-interactive partitioning.""" self._ready = False hubQ.send_not_ready(self.__class__.__name__) report = apply_partitioning(partitioning=self._partitioning, show_message_cb=self._show_execute_message, reset_storage_cb=self._reset_storage) StorageCheckHandler.errors = list(report.error_messages) StorageCheckHandler.warnings = list(report.warning_messages) self._ready = True hubQ.send_ready(self.__class__.__name__) def _show_execute_message(self, msg): hubQ.send_message(self.__class__.__name__, msg) log.debug(msg) def _reset_storage(self): reset_storage(scan_all=True) @property def completed(self): return self.ready and not self.errors and self._device_tree.GetRootDevice( ) @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready \ and not threadMgr.get(constants.THREAD_STORAGE) \ and not threadMgr.get(constants.THREAD_DASDFMT) \ and not threadMgr.get(constants.THREAD_EXECUTE_STORAGE) @property def status(self): """ A short string describing the current status of storage setup. """ if not self.ready: return _("Processing...") elif flags.automatedInstall and not self._device_tree.GetRootDevice(): return _("Kickstart insufficient") elif not self._disk_select_module.SelectedDisks: return _("No disks selected") elif self.errors: return _("Error checking storage configuration") elif self.warnings: return _("Warning checking storage configuration") elif self._last_partitioning_method == PARTITIONING_METHOD_AUTOMATIC: return _("Automatic partitioning selected") else: return _("Custom partitioning selected") @property def local_overviews(self): return self._local_disks_box.get_children() @property def advanced_overviews(self): return [ child for child in self._specialized_disks_box.get_children() if isinstance(child, AnacondaWidgets.DiskOverview) ] def _on_disk_clicked(self, overview, event): # This handler only runs for these two kinds of events, and only for # activate-type keys (space, enter) in the latter event's case. if event.type not in [ Gdk.EventType.BUTTON_PRESS, Gdk.EventType.KEY_RELEASE ]: return if event.type == Gdk.EventType.KEY_RELEASE and \ event.keyval not in [Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_ISO_Enter, Gdk.KEY_KP_Enter, Gdk.KEY_KP_Space]: return if event.type == Gdk.EventType.BUTTON_PRESS and \ event.state & Gdk.ModifierType.SHIFT_MASK: # clicked with Shift held down if self._last_clicked_overview is None: # nothing clicked before, cannot apply Shift-click return local_overviews = self.local_overviews advanced_overviews = self.advanced_overviews # find out which list of overviews the clicked one belongs to if overview in local_overviews: from_overviews = local_overviews elif overview in advanced_overviews: from_overviews = advanced_overviews else: # should never happen, but if it does, no other actions should be done return if self._last_clicked_overview in from_overviews: # get index of the last clicked overview last_idx = from_overviews.index(self._last_clicked_overview) else: # overview from the other list clicked before, cannot apply "Shift-click" return # get index and state of the clicked overview cur_idx = from_overviews.index(overview) state = self._last_clicked_overview.get_chosen() if cur_idx > last_idx: copy_to = from_overviews[last_idx:cur_idx + 1] else: copy_to = from_overviews[cur_idx:last_idx] # copy the state of the last clicked overview to the ones between it and the # one clicked with the Shift held down for disk_overview in copy_to: disk_overview.set_chosen(state) self._update_disk_list() self._update_summary() def _on_disk_focus_in(self, overview, event): self._last_clicked_overview = self._cur_clicked_overview self._cur_clicked_overview = overview def refresh(self): self._back_clicked = False self._available_disks = self._disk_select_module.GetUsableDisks() self._selected_disks = self._disk_select_module.SelectedDisks # Get the available selected disks. self._selected_disks = filter_disks_by_names(self._available_disks, self._selected_disks) # First, remove all non-button children. for child in self.local_overviews + self.advanced_overviews: child.destroy() # Then deal with local disks, which are really easy. They need to be # handled here instead of refresh to take into account the user pressing # the rescan button on custom partitioning. # Advanced disks are different. Because there can potentially be a lot # of them, we do not display them in the box by default. Instead, only # those selected in the filter UI are displayed. This means refresh # needs to know to create and destroy overviews as appropriate. for device_name in self._available_disks: # Get the device data. device_data = DeviceData.from_structure( self._device_tree.GetDeviceData(device_name)) if is_local_disk(device_data.type): # Add all available local disks. self._add_disk_overview(device_data, self._local_disks_box) elif device_name in self._selected_disks: # Add only selected advanced disks. self._add_disk_overview(device_data, self._specialized_disks_box) # update the selections in the ui for overview in self.local_overviews + self.advanced_overviews: name = overview.get_property("name") overview.set_chosen(name in self._selected_disks) # Update the encryption checkbox. if self._partitioning_request.encrypted: self._encrypted_checkbox.set_active(True) self._update_summary() self._check_problems() def _check_problems(self): if self.errors: self.set_warning( _("Error checking storage configuration. " "<a href=\"\">Click for details.</a>")) return True elif self.warnings: self.set_warning( _("Warning checking storage configuration. " "<a href=\"\">Click for details.</a>")) return True return False def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # Connect the viewport adjustments to the child widgets # See also https://bugzilla.gnome.org/show_bug.cgi?id=744721 self._local_disks_box.set_focus_hadjustment( Gtk.Scrollable.get_hadjustment(self._local_viewport)) self._specialized_disks_box.set_focus_hadjustment( Gtk.Scrollable.get_hadjustment(self._specialized_viewport)) self._main_box.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(self._main_viewport)) threadMgr.add( AnacondaThread(name=constants.THREAD_STORAGE_WATCHER, target=self._initialize)) def _add_disk_overview(self, device_data, box): if device_data.type == "dm-multipath": # We don't want to display the whole huge WWID for a multipath device. wwn = device_data.attrs.get("wwn", "") description = wwn[0:6] + "..." + wwn[-8:] elif device_data.type == "zfcp": # Manually mangle the desc of a zFCP device to be multi-line since # it's so long it makes the disk selection screen look odd. description = _( "FCP device {hba_id}\nWWPN {wwpn}\nLUN {lun}").format( hba_id=device_data.attrs.get("hba-id", ""), wwpn=device_data.attrs.get("wwpn", ""), lun=device_data.attrs.get("fcp-lun", "")) elif device_data.type == "nvdimm": description = _("NVDIMM device {namespace}").format( namespace=device_data.attrs.get("namespace", "")) else: description = device_data.description kind = "drive-removable-media" if device_data.removable else "drive-harddisk" free_space = self._device_tree.GetDiskFreeSpace([device_data.name]) serial_number = device_data.attrs.get("serial") or None overview = AnacondaWidgets.DiskOverview( description, kind, str(Size(device_data.size)), _("{} free").format(str(Size(free_space))), device_data.name, serial_number) box.pack_start(overview, False, False, 0) overview.set_chosen(device_data.name in self._selected_disks) overview.connect("button-press-event", self._on_disk_clicked) overview.connect("key-release-event", self._on_disk_clicked) overview.connect("focus-in-event", self._on_disk_focus_in) overview.show_all() def _initialize(self): """Finish the initialization. This method is expected to run only once during the initialization. """ # Wait for storage. hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PROBING_STORAGE)) threadMgr.wait(constants.THREAD_STORAGE) # Automatically format DASDs if allowed. disks = self._disk_select_module.GetUsableDisks() DasdFormatting.run_automatically(disks, self._show_dasdfmt_report) hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PROBING_STORAGE)) # Update the selected disks. select_default_disks() # Automatically apply the preconfigured partitioning. # Do not set ready in the automated installation before # the execute method is run. if flags.automatedInstall and self._is_preconfigured: self._check_required_passphrase() self.execute() else: self._ready = True hubQ.send_ready(self.__class__.__name__) # Report that the storage spoke has been initialized. self.initialize_done() def _show_dasdfmt_report(self, msg): hubQ.send_message(self.__class__.__name__, msg) @async_action_wait def _check_required_passphrase(self): """Ask a user for a default passphrase if required.""" if not is_passphrase_required(self._partitioning): return dialog = PassphraseDialog(self.data) with self.main_window.enlightbox(dialog.window): rc = dialog.run() if rc != 1: return set_required_passphrase(self._partitioning, dialog.passphrase) def _update_summary(self): """ Update the summary based on the UI. """ disks = filter_disks_by_names(self._available_disks, self._selected_disks) summary = get_disks_summary(disks) summary_label = self.builder.get_object("summary_label") summary_label.set_text(summary) is_selected = bool(self._selected_disks) summary_label.set_sensitive(is_selected) # only show the "we won't touch your other disks" labels and summary button when # some disks are selected self.builder.get_object("summary_button_revealer").set_reveal_child( is_selected) self.builder.get_object( "local_untouched_label_revealer").set_reveal_child(is_selected) self.builder.get_object( "special_untouched_label_revealer").set_reveal_child(is_selected) self.builder.get_object("other_options_grid").set_sensitive( is_selected) if not self._available_disks: self.set_warning(_(WARNING_NO_DISKS_DETECTED)) elif not self._selected_disks: # There may be an underlying reason that no disks were selected, give them priority. if not self._check_problems(): self.set_warning(_(WARNING_NO_DISKS_SELECTED)) else: self.clear_info() def _update_disk_list(self): """ Update self.selected_disks based on the UI. """ for overview in self.local_overviews + self.advanced_overviews: selected = overview.get_chosen() name = overview.get_property("name") if selected and name not in self._selected_disks: self._selected_disks.append(name) if not selected and name in self._selected_disks: self._selected_disks.remove(name) # signal handlers def on_summary_clicked(self, button): # show the selected disks dialog disks = filter_disks_by_names(self._available_disks, self._selected_disks) dialog = SelectedDisksDialog(self.data, disks) dialog.refresh() self.run_lightbox_dialog(dialog) # update selected disks since some may have been removed self._selected_disks = list(dialog.disks) # update the UI to reflect changes to self.selected_disks for overview in self.local_overviews + self.advanced_overviews: name = overview.get_property("name") overview.set_chosen(name in self._selected_disks) self._update_summary() if self._bootloader_module.BootloaderMode != BOOTLOADER_ENABLED: self.set_warning( _("You have chosen to skip boot loader installation. " "Your system may not be bootable.")) else: self.clear_info() def run_lightbox_dialog(self, dialog): with self.main_window.enlightbox(dialog.window): rc = dialog.run() return rc def _check_dasd_formats(self): # No change by default. rc = DASD_FORMAT_NO_CHANGE # Do nothing if unsupported. if not DasdFormatting.is_supported(): return rc # Allow to format DASDs. self._disk_init_module.FormatUnrecognizedEnabled = True self._disk_init_module.FormatLDLEnabled = True # Get selected disks. disks = filter_disks_by_names(self._available_disks, self._selected_disks) # Check if some of the disks should be formatted. dasd_formatting = DasdFormatting() dasd_formatting.search_disks(disks) if dasd_formatting.should_run(): # We want to apply current selection before running dasdfmt to # prevent this information from being lost afterward apply_disk_selection(self._selected_disks) # Run the dialog. dialog = DasdFormatDialog(self.data, dasd_formatting) ignoreEscape(dialog.window) rc = self.run_lightbox_dialog(dialog) return rc def _check_space_and_run_dialog(self, partitioning, disks): # User wants to reclaim the space. if self._reclaim_checkbox.get_active(): return RESPONSE_RECLAIM # Get the device tree of the partitioning module. device_tree = STORAGE.get_proxy(partitioning.GetDeviceTree()) # Calculate the required and free space. disk_free = Size(device_tree.GetDiskFreeSpace(disks)) fs_free = Size(device_tree.GetDiskReclaimableSpace(disks)) disks_size = Size(device_tree.GetDiskTotalSpace(disks)) sw_space = Size(self.payload.space_required) auto_swap = suggest_swap_size() log.debug("disk free: %s fs free: %s sw needs: %s auto swap: %s", disk_free, fs_free, sw_space, auto_swap) # We need enough space for the software, the swap and the metadata. # It is not an ideal estimate, but it works. required_space = sw_space + auto_swap + STORAGE_METADATA_RATIO * disk_free # There is enough space to continue. if disk_free >= required_space: return RESPONSE_OK # Ask user what to do. if disks_size >= required_space - auto_swap: dialog = NeedSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, sw_space, auto_swap, disk_free, fs_free) else: dialog = NoSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, sw_space, auto_swap, disk_free, fs_free) return self.run_lightbox_dialog(dialog) def on_back_clicked(self, button): if self._back_clicked: return # Skip if user is clicking multiple times on the back button. self._back_clicked = True # Clear the current warning message if any. self.clear_info() # No disks selected? The user wants to back out of the storage spoke. if not self._selected_disks: NormalSpoke.on_back_clicked(self, button) return # Reset to a snapshot if necessary. self._reset_to_snapshot() # The disk selection has to make sense before we can proceed. if not self._check_disk_selection(): self._back_clicked = False return # Check for unsupported DASDs. rc = self._check_dasd_formats() if rc == DASD_FORMAT_NO_CHANGE: pass elif rc == DASD_FORMAT_REFRESH: # User hit OK on the dialog self.refresh() elif rc == DASD_FORMAT_RETURN_TO_HUB: # User clicked uri to return to hub. NormalSpoke.on_back_clicked(self, button) return else: # User either hit cancel on the dialog or closed # it via escape, there was no formatting done. self._back_clicked = False return # Handle the partitioning. partitioning_method = self._get_selected_partitioning_method() self._last_partitioning_method = partitioning_method if partitioning_method == PARTITIONING_METHOD_AUTOMATIC: self._skip_to_automatic_partitioning() return if partitioning_method == PARTITIONING_METHOD_INTERACTIVE: self._skip_to_spoke("CustomPartitioningSpoke") return if partitioning_method == PARTITIONING_METHOD_BLIVET: self._skip_to_spoke("BlivetGuiSpoke") return self._back_clicked = False return def _reset_to_snapshot(self): # Can we reset the storage configuration? reset = False # Changing disk selection is really, really complicated and has # always been causing numerous hard bugs. Let's not play the hero # game and just revert everything and start over again. disks = self._last_selected_disks current_disks = set(self._selected_disks) self._last_selected_disks = set(current_disks) if disks and disks != current_disks: log.info("Disk selection has changed.") reset = True method = self._last_partitioning_method current_method = self._get_selected_partitioning_method() self._last_partitioning_method = current_method # Same thing for switching between different storage configuration # methods (auto/custom/blivet-gui), at least for now. if method != current_method: log.info("Partitioning method has changed from %s to %s.", method, current_method) reset = True # Reset the storage configuration if necessary. # FIXME: Reset only the partitioning that we will use. if reset: log.info("Rolling back planed storage configuration changes.") self._storage_module.ResetPartitioning() def _check_disk_selection(self): # If there are some disk selection errors we don't let user to leave # the spoke, so these errors don't have to go to self.errors. report = ValidationReport.from_structure( self._disk_select_module.ValidateSelectedDisks( self._selected_disks)) if not report.is_valid(): self._disks_errors = report.get_messages() self.set_error( _("There was a problem with your disk selection. " "Click here for details.")) return False self._disks_errors = [] return True def _skip_to_spoke(self, name, apply_on_skip=True): """Skip to a spoke. The user has requested to skip to different spoke or to the summary hub. :param name: a name of the spoke or None to return to the hub :param apply_on_skip: should we call apply? """ self.skipTo = name self.applyOnSkip = apply_on_skip NormalSpoke.on_back_clicked(self, None) def _skip_to_automatic_partitioning(self): """Skip to the automatic partitioning. The user has requested to create the partitioning automatically. Ask for missing information and set up the automatic partitioning, so it can be later applied in the execute method. """ # Set up the encryption. self._partitioning_request.encrypted = self._encrypted_checkbox.get_active( ) # Ask for a passphrase. if self._partitioning_request.encrypted: dialog = PassphraseDialog(self.data, self._partitioning_request.passphrase) rc = self.run_lightbox_dialog(dialog) if rc != 1: self._back_clicked = False return self._partitioning_request.passphrase = dialog.passphrase # Set up the disk selection and initialization. self.apply() # Use the automatic partitioning and reset it. self._partitioning = create_partitioning(PARTITIONING_METHOD_AUTOMATIC) self._partitioning.Request = \ PartitioningRequest.to_structure(self._partitioning_request) # Reclaim space. disks = filter_disks_by_names(self._available_disks, self._selected_disks) rc = self._check_space_and_run_dialog(self._partitioning, disks) if rc == RESPONSE_RECLAIM: dialog = ResizeDialog(self.data, self.payload, self._partitioning, disks) dialog.refresh() rc = self.run_lightbox_dialog(dialog) # Plan the next action. if rc == RESPONSE_OK: # nothing special needed self._skip_to_spoke(None) return if rc == RESPONSE_CANCEL: # A cancel button was clicked on one of the dialogs. Stay on this # spoke. Generally, this is because the user wants to add more disks. self._back_clicked = False return if rc == RESPONSE_MODIFY_SW: # The "Fedora software selection" link was clicked on one of the # dialogs. Send the user to the software spoke. self._skip_to_spoke("SoftwareSelectionSpoke") return if rc == RESPONSE_QUIT: # Not enough space, and the user can't do anything about it so # they chose to quit. raise SystemExit("user-selected exit") # I don't know how we'd get here, but might as well have a # catch-all. Just stay on this spoke. self._back_clicked = False return def on_specialized_clicked(self, button): # Don't want to run apply or execute in this case, since we have to # collect some more disks first. The user will be back to this spoke. self.applyOnSkip = False # However, we do want to apply current selections so the disk cart off # the filter spoke will display the correct information. apply_disk_selection(self._selected_disks) self.skipTo = "FilterSpoke" NormalSpoke.on_back_clicked(self, button) def on_info_bar_clicked(self, *args): if self._disks_errors: label = _( "The following errors were encountered when checking your disk " "selection. You can modify your selection or quit the " "installer.") dialog = DetailedErrorDialog(self.data, buttons=[ C_("GUI|Storage|Error Dialog", "_Quit"), C_("GUI|Storage|Error Dialog", "_Modify Disk Selection") ], label=label) with self.main_window.enlightbox(dialog.window): errors = "\n".join(self._disks_errors) dialog.refresh(errors) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. util.ipmi_abort(scripts=self.data.scripts) sys.exit(0) elif self.errors: label = _( "The following errors were encountered when checking your storage " "configuration. You can modify your storage layout or quit the " "installer.") dialog = DetailedErrorDialog(self.data, buttons=[ C_("GUI|Storage|Error Dialog", "_Quit"), C_("GUI|Storage|Error Dialog", "_Modify Storage Layout") ], label=label) with self.main_window.enlightbox(dialog.window): errors = "\n".join(self.errors) dialog.refresh(errors) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. util.ipmi_abort(scripts=self.data.scripts) sys.exit(0) elif self.warnings: label = _( "The following warnings were encountered when checking your storage " "configuration. These are not fatal, but you may wish to make " "changes to your storage layout.") dialog = DetailedErrorDialog( self.data, buttons=[C_("GUI|Storage|Warning Dialog", "_OK")], label=label) with self.main_window.enlightbox(dialog.window): warnings = "\n".join(self.warnings) dialog.refresh(warnings) rc = dialog.run() dialog.window.destroy() def on_disks_key_released(self, box, event): # we want to react only on Ctrl-A being pressed if not bool(event.state & Gdk.ModifierType.CONTROL_MASK) or \ (event.keyval not in (Gdk.KEY_a, Gdk.KEY_A)): return # select disks in the right box if box is self._local_disks_box: overviews = self.local_overviews elif box is self._specialized_disks_box: overviews = self.advanced_overviews else: # no other box contains disk overviews return for overview in overviews: overview.set_chosen(True) self._update_disk_list() self._update_summary() # This callback is for the button that has anaconda go back and rescan the # disks to pick up whatever changes the user made outside our control. def on_refresh_clicked(self, *args): dialog = RefreshDialog(self.data) ignoreEscape(dialog.window) with self.main_window.enlightbox(dialog.window): rc = dialog.run() dialog.window.destroy() if rc == 1: # User hit OK on the dialog, indicating they stayed on the dialog # until rescanning completed. self.refresh() return elif rc != 2: # User either hit cancel on the dialog or closed it via escape, so # there was no rescanning done. # NOTE: rc == 2 means the user clicked on the link that takes them # back to the hub. return # Can't use this spoke's on_back_clicked method as that will try to # save the right hand side, which is no longer valid. The user must # go back and select their disks all over again since whatever they # did on the shell could have changed what disks are available. NormalSpoke.on_back_clicked(self, None)
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: PasswordSpoke :parts: 3 """ builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" focusWidgetName = "password_entry" uiFile = "spokes/root_password.glade" helpFile = "PasswordSpoke.xml" category = UserSettingsCategory icon = "dialog-password-symbolic" title = CN_("GUI|Spoke", "_Root Password") def __init__(self, *args): NormalSpoke.__init__(self, *args) GUISpokeInputCheckHandler.__init__(self) self._users_module = USERS.get_observer() self._users_module.connect() self._services_module = SERVICES.get_observer() self._services_module.connect() def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # get object references from the builders self._password_entry = self.builder.get_object("password_entry") self._password_confirmation_entry = self.builder.get_object("password_confirmation_entry") self._password_bar = self.builder.get_object("password_bar") self._password_label = self.builder.get_object("password_label") # set state based on kickstart # NOTE: this will stop working once the module supports multiple kickstart commands self.password_kickstarted = self._users_module.proxy.IsRootpwKickstarted # Install the password checks: # - Has a password been specified? # - If a password has been specified and there is data in the confirm box, do they match? # - How strong is the password? # - Does the password contain non-ASCII characters? # Setup the password checker for password checking self._checker = input_checking.PasswordChecker( initial_password_content = self.password, initial_password_confirmation_content = self.password_confirmation, policy = input_checking.get_policy(self.data, "root") ) # configure the checker for password checking self.checker.secret_type = constants.SecretType.PASSWORD # remove any placeholder texts if either password or confirmation field changes content from initial state self.checker.password.changed_from_initial_state.connect(self.remove_placeholder_texts) self.checker.password_confirmation.changed_from_initial_state.connect(self.remove_placeholder_texts) # connect UI updates to check results self.checker.checks_done.connect(self._checks_done) # check that the password is not empty self._empty_check = input_checking.PasswordEmptyCheck() # check that the content of the password field & the conformation field are the same self._confirm_check = input_checking.PasswordConfirmationCheck() # check password validity, quality and strength self._validity_check = input_checking.PasswordValidityCheck() # connect UI updates to validity check results self._validity_check.result.password_score_changed.connect(self.set_password_score) self._validity_check.result.status_text_changed.connect(self.set_password_status) # check if the password contains non-ascii characters self._ascii_check = input_checking.PasswordASCIICheck() # register the individual checks with the checker in proper order # 1) is the password non-empty ? # 2) are both entered passwords the same ? # 3) is the password valid according to the current password checking policy ? # 4) is the password free of non-ASCII characters ? self.checker.add_check(self._empty_check) self.checker.add_check(self._confirm_check) self.checker.add_check(self._validity_check) self.checker.add_check(self._ascii_check) # set placeholders if the password has been kickstarted as we likely don't know # nothing about it and can't really show it in the UI in any meaningful way password_set_message = _("The password was set by kickstart.") if self.password_kickstarted: self.password_entry.set_placeholder_text(password_set_message) self.password_confirmation_entry.set_placeholder_text(password_set_message) # Configure levels for the password bar self._password_bar.add_offset_value("low", 2) self._password_bar.add_offset_value("medium", 3) self._password_bar.add_offset_value("high", 4) # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__, False) # report that we are done self.initialize_done() def refresh(self): # focus on the password field if password was not kickstarted if not self.password_kickstarted: self.password_entry.grab_focus() # rerun checks so that we have a correct status message, if any self.checker.run_checks() @property def status(self): if self._users_module.proxy.IsRootAccountLocked: # check if we are running in Initial Setup reconfig mode reconfig_mode = self._services_module.proxy.SetupOnBoot == constants.SETUP_ON_BOOT_RECONFIG # reconfig mode currently allows re-enabling a locked root account if # user sets a new root password if reconfig_mode: return _("Disabled, set password to enable.") else: return _("Root account is disabled.") elif self._users_module.proxy.IsRootPasswordSet: return _("Root password is set") else: return _("Root password is not set") @property def mandatory(self): return not any(user for user in self.data.user.userList if "wheel" in user.groups) def apply(self): pw = self.password # value from the kickstart changed # NOTE: yet again, this stops to be valid once multiple # commands are supported by a single DBUS module self._users_module.proxy.SetRootpwKickstarted(False) self.password_kickstarted = False self._users_module.proxy.SetRootAccountLocked(False) if not pw: self._users_module.proxy.ClearRootPassword() return # we have a password - set it to kickstart data self._users_module.proxy.SetCryptedRootPassword(cryptPassword(pw)) # clear any placeholders self.remove_placeholder_texts() # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__, False) @property def completed(self): return bool(self._users_module.proxy.IsRootPasswordSet or self._users_module.proxy.IsRootAccountLocked) @property def sensitive(self): return not (self.completed and flags.automatedInstall and self._users_module.proxy.IsRootpwKickstarted) def _checks_done(self, error_message): """Update the warning with the input validation error from the first error message or clear warnings if all the checks were successful. Also appends the "press twice" suffix if compatible with current password policy and handles the press-done-twice logic. """ # check if an unwaivable check failed unwaivable_check_failed = not self._confirm_check.result.success # set appropriate status bar message if not error_message: # all is fine, just clear the message self.clear_info() elif not self.password and not self.password_confirmation: # Clear any info message if both the password and password # confirmation fields are empty. # This shortcut is done to make it possible for the user to leave the spoke # without inputting any root password. Separate logic makes sure an # empty string is not set as the root password. self.clear_info() else: if self.checker.policy.strict or unwaivable_check_failed: # just forward the error message self.show_warning_message(error_message) else: # add suffix for the click twice logic self.show_warning_message("{} {}".format(error_message, _(constants.PASSWORD_DONE_TWICE))) # check if the spoke can be exited after the latest round of checks self._check_spoke_exit_conditions(unwaivable_check_failed) def _check_spoke_exit_conditions(self, unwaivable_check_failed): # Check if the user can escape from the root spoke or stay forever ! # reset any waiving in progress self.waive_clicks = 0 # Depending on the policy we allow users to waive the password strength # and non-ASCII checks. If the policy is set to strict, the password # needs to be strong, but can still contain non-ASCII characters. self.can_go_back = False self.needs_waiver = True # This shortcut is done to make it possible for the user to leave the spoke # without inputting any root password. Separate logic makes sure an # empty string is not set as the root password. if not self.password and not self.password_confirmation: self.can_go_back = True self.needs_waiver = False elif self.checker.success: # if all checks were successful we can always go back to the hub self.can_go_back = True self.needs_waiver = False elif unwaivable_check_failed: self.can_go_back = False else: if self.checker.policy.strict: if not self._validity_check.result.success: # failing validity check in strict # mode prevents us from going back self.can_go_back = False elif not self._ascii_check.result.success: # but the ASCII check can still be waived self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False else: if not self._validity_check.result.success: self.can_go_back = True self.needs_waiver = True elif not self._ascii_check.result.success: self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False def on_password_changed(self, editable, data=None): """Tell checker that the content of the password field changed.""" self.checker.password.content = self.password def on_password_confirmation_changed(self, editable, data=None): """Tell checker that the content of the password confirmation field changed.""" self.checker.password_confirmation.content = self.password_confirmation def on_password_icon_clicked(self, entry, icon_pos, event): """Called by Gtk callback when the icon of a password entry is clicked.""" set_password_visibility(entry, not entry.get_visibility()) def on_back_clicked(self, button): # the GUI spoke input check handler handles the spoke exit logic for us if self.try_to_go_back(): NormalSpoke.on_back_clicked(self, button) else: log.info("Return to hub prevented by password checking rules.")
def get_container_type(device_type): return CONTAINER_TYPES.get(device_type, ContainerType(N_("container"), CN_( "GUI|Custom Partitioning|Configure|Devices", "container")))
NOTEBOOK_LUKS_PAGE = 2 NOTEBOOK_UNEDITABLE_PAGE = 3 NOTEBOOK_INCOMPLETE_PAGE = 4 NEW_CONTAINER_TEXT = N_("Create a new %(container_type)s ...") CONTAINER_TOOLTIP = N_("Create or select %(container_type)s") CONTAINER_DIALOG_TITLE = N_("CONFIGURE %(container_type)s") CONTAINER_DIALOG_TEXT = N_("Please create a name for this %(container_type)s " "and select at least one disk below.") ContainerType = namedtuple("ContainerType", ["name", "label"]) CONTAINER_TYPES = { DEVICE_TYPE_LVM: ContainerType( N_("Volume Group"), CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume Group:")), DEVICE_TYPE_LVM_THINP: ContainerType( N_("Volume Group"), CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume Group:")), DEVICE_TYPE_BTRFS: ContainerType( N_("Volume"), CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume:")) } def get_size_from_entry(entry, lower_bound=None, units=None): """ Get a Size object from an entry field. :param lower_bound: lower bound for size returned, :type lower_bound: :class:`blivet.size.Size` or NoneType :param units: units to use if none obtained from entry
class FilterSpoke(NormalSpoke): """ .. inheritance-diagram:: FilterSpoke :parts: 3 """ builderObjects = ["diskStore", "filterWindow", "searchModel", "multipathModel", "otherModel", "zModel", "nvdimmModel"] mainWidgetName = "filterWindow" uiFile = "spokes/advanced_storage.glade" helpFile = "FilterSpoke.xml" category = SystemCategory title = CN_("GUI|Spoke", "_Installation Destination") @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "storage-advanced-configuration" def __init__(self, *args): super().__init__(*args) self.applyOnSkip = True self._pages = {} self._ancestors = [] self._disks = [] self._selected_disks = [] self._protected_disks = [] self._storage_module = STORAGE.get_proxy() self._device_tree = STORAGE.get_proxy(DEVICE_TREE) self._disk_selection = STORAGE.get_proxy(DISK_SELECTION) self._notebook = self.builder.get_object("advancedNotebook") self._store = self.builder.get_object("diskStore") self._reconfigure_nvdimm_button = self.builder.get_object("reconfigureNVDIMMButton") @property def indirect(self): return True # This spoke has no status since it's not in a hub @property def status(self): return None def apply(self): apply_disk_selection(self._selected_disks) def initialize(self): super().initialize() self.initialize_start() self._pages = { PAGE_SEARCH: SearchPage(self.builder), PAGE_MULTIPATH: MultipathPage(self.builder), PAGE_OTHER: OtherPage(self.builder), PAGE_NVDIMM: NvdimmPage(self.builder), PAGE_Z: ZPage(self.builder), } if not STORAGE.get_proxy(DASD).IsSupported(): self._notebook.remove_page(PAGE_Z) self._pages.pop(PAGE_Z) self.builder.get_object("addZFCPButton").destroy() self.builder.get_object("addDASDButton").destroy() if not STORAGE.get_proxy(FCOE).IsSupported(): self.builder.get_object("addFCOEButton").destroy() if not STORAGE.get_proxy(ISCSI).IsSupported(): self.builder.get_object("addISCSIButton").destroy() # The button is sensitive only on NVDIMM page self._reconfigure_nvdimm_button.set_sensitive(False) # report that we are done self.initialize_done() def refresh(self): super().refresh() # Reset the scheduled partitioning if any to make sure that we # are working with the current system’s storage configuration. # FIXME: Change modules and UI to work with the right device tree. self._storage_module.ResetPartitioning() self._disks = self._disk_selection.GetUsableDisks() self._selected_disks = self._disk_selection.SelectedDisks self._protected_disks = self._disk_selection.ProtectedDevices self._ancestors = self._device_tree.GetAncestors(self._disks) # Now all all the non-local disks to the store. Everything has been set up # ahead of time, so there's no need to configure anything. We first make # these lists of disks, then call setup on each individual page. This is # because there could be page-specific setup to do that requires a complete # view of all the disks on that page. self._store.clear() disks_data = DeviceData.from_structure_list([ self._device_tree.GetDeviceData(device_name) for device_name in self._disks ]) for page in self._pages.values(): disks = [ d for d in disks_data if page.is_member(d.type) ] page.setup( self._store, disks, self._selected_disks, self._protected_disks ) self._update_summary() def _update_summary(self): summary_button = self.builder.get_object("summary_button") label = self.builder.get_object("summary_button_label") # We need to remove ancestor devices from the count. Otherwise, we'll # end up in a situation where selecting one multipath device could # potentially show three devices selected (mpatha, sda, sdb for instance). count = len([ disk for disk in self._selected_disks if disk not in self._ancestors ]) summary = CP_( "GUI|Installation Destination|Filter", "{} _storage device selected", "{} _storage devices selected", count ).format(count) if count > 0: really_show(summary_button) label.set_text(summary) label.set_use_underline(True) else: really_hide(summary_button) def on_back_clicked(self, button): self.skipTo = "StorageSpoke" super().on_back_clicked(button) def on_summary_clicked(self, button): disks = filter_disks_by_names( self._disks, self._selected_disks ) dialog = SelectedDisksDialog( self.data, disks, show_remove=False, set_boot=False ) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() def on_clear_icon_clicked(self, entry, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: entry.set_text("") def on_page_switched(self, notebook, new_page, new_page_num, *args): # Disable all filters. for page in self._pages.values(): page.is_active = False # Set up the new page. page = self._pages[new_page_num] page.is_active = True page.model.refilter() log.debug("Show the page %s.", str(page)) # Set up the UI. notebook.get_nth_page(new_page_num).show_all() self._reconfigure_nvdimm_button.set_sensitive(new_page_num == 3) def on_row_toggled(self, button, path): if not path: return page_index = self._notebook.get_current_page() filter_model = self._pages[page_index].model model_itr = filter_model.get_iter(path) itr = filter_model.convert_iter_to_child_iter(model_itr) self._store[itr][1] = not self._store[itr][1] if self._store[itr][1] and self._store[itr][3] not in self._selected_disks: self._selected_disks.append(self._store[itr][3]) elif not self._store[itr][1] and self._store[itr][3] in self._selected_disks: self._selected_disks.remove(self._store[itr][3]) self._update_summary() @timed_action(delay=50, threshold=100) def on_refresh_clicked(self, widget, *args): log.debug("Refreshing...") try_populate_devicetree() self.refresh() def on_add_iscsi_clicked(self, widget, *args): log.debug("Add a new iSCSI device.") dialog = ISCSIDialog(self.data) self._run_dialog_and_refresh(dialog) def on_add_fcoe_clicked(self, widget, *args): log.debug("Add a new FCoE device.") dialog = FCoEDialog(self.data) self._run_dialog_and_refresh(dialog) def on_add_zfcp_clicked(self, widget, *args): log.debug("Add a new zFCP device.") dialog = ZFCPDialog(self.data) self._run_dialog_and_refresh(dialog) def on_add_dasd_clicked(self, widget, *args): log.debug("Add a new DASD device.") dialog = DASDDialog(self.data) self._run_dialog_and_refresh(dialog) def on_reconfigure_nvdimm_clicked(self, widget, *args): log.debug("Reconfigure a NVDIMM device.") namespaces = self._pages[PAGE_NVDIMM].get_selected_namespaces() dialog = NVDIMMDialog(self.data, namespaces) self._run_dialog_and_refresh(dialog) def _run_dialog_and_refresh(self, dialog): # Run the dialog. with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() @timed_action(delay=1200, busy_cursor=False) def on_filter_changed(self, *args): self._refilter_current_page() def on_search_type_changed(self, combo): self._set_notebook_page("searchTypeNotebook", combo.get_active()) self._refilter_current_page() def on_multipath_type_changed(self, combo): self._set_notebook_page("multipathTypeNotebook", combo.get_active()) self._refilter_current_page() def on_other_type_combo_changed(self, combo): self._set_notebook_page("otherTypeNotebook", combo.get_active()) self._refilter_current_page() def on_nvdimm_type_combo_changed(self, combo): self._set_notebook_page("nvdimmTypeNotebook", combo.get_active()) self._refilter_current_page() def on_z_type_combo_changed(self, combo): self._set_notebook_page("zTypeNotebook", combo.get_active()) self._refilter_current_page() def _set_notebook_page(self, notebook_name, page_index): notebook = self.builder.get_object(notebook_name) notebook.set_current_page(page_index) self._refilter_current_page() def _refilter_current_page(self): index = self._notebook.get_current_page() page = self._pages[index] page.model.refilter()
class FilterSpoke(NormalSpoke): """ .. inheritance-diagram:: FilterSpoke :parts: 3 """ builderObjects = [ "diskStore", "filterWindow", "searchModel", "multipathModel", "otherModel", "zModel" ] mainWidgetName = "filterWindow" uiFile = "spokes/advanced_storage.glade" helpFile = "FilterSpoke.xml" category = SystemCategory title = CN_("GUI|Spoke", "_INSTALLATION DESTINATION") def __init__(self, *args): NormalSpoke.__init__(self, *args) self.applyOnSkip = True self.ancestors = [] self.disks = [] self.selected_disks = [] @property def indirect(self): return True # This spoke has no status since it's not in a hub @property def status(self): return None def apply(self): applyDiskSelection(self.storage, self.data, self.selected_disks) # some disks may have been added in this spoke, we need to recreate the # snapshot of on-disk storage if on_disk_storage.created: on_disk_storage.dispose_snapshot() on_disk_storage.create_snapshot(self.storage) def initialize(self): NormalSpoke.initialize(self) self.initialize_start() self.pages = [ SearchPage(self.storage, self.builder), MultipathPage(self.storage, self.builder), OtherPage(self.storage, self.builder), ZPage(self.storage, self.builder) ] self._notebook = self.builder.get_object("advancedNotebook") if not arch.is_s390(): self._notebook.remove_page(-1) self.builder.get_object("addZFCPButton").destroy() self.builder.get_object("addDASDButton").destroy() if not has_fcoe(): self.builder.get_object("addFCOEButton").destroy() if not iscsi.available: self.builder.get_object("addISCSIButton").destroy() self._store = self.builder.get_object("diskStore") self._addDisksButton = self.builder.get_object("addDisksButton") # report that we are done self.initialize_done() def _real_ancestors(self, disk): # Return a list of all the ancestors of a disk, but remove the disk # itself from this list. return [d for d in disk.ancestors if d.name != disk.name] def refresh(self): NormalSpoke.refresh(self) self.disks = getDisks(self.storage.devicetree) self.selected_disks = self.data.ignoredisk.onlyuse[:] self.ancestors = [ d.name for disk in self.disks for d in self._real_ancestors(disk) ] self._store.clear() allDisks = [] multipathDisks = [] otherDisks = [] zDisks = [] # Now all all the non-local disks to the store. Everything has been set up # ahead of time, so there's no need to configure anything. We first make # these lists of disks, then call setup on each individual page. This is # because there could be page-specific setup to do that requires a complete # view of all the disks on that page. for disk in self.disks: if self.pages[1].ismember(disk): multipathDisks.append(disk) elif self.pages[2].ismember(disk): otherDisks.append(disk) elif self.pages[3].ismember(disk): zDisks.append(disk) allDisks.append(disk) self.pages[0].setup(self._store, self.selected_disks, allDisks) self.pages[1].setup(self._store, self.selected_disks, multipathDisks) self.pages[2].setup(self._store, self.selected_disks, otherDisks) self.pages[3].setup(self._store, self.selected_disks, zDisks) self._update_summary() def _update_summary(self): summaryButton = self.builder.get_object("summary_button") label = self.builder.get_object("summary_button_label") # We need to remove ancestor devices from the count. Otherwise, we'll # end up in a situation where selecting one multipath device could # potentially show three devices selected (mpatha, sda, sdb for instance). count = len([ disk for disk in self.selected_disks if disk not in self.ancestors ]) summary = CP_("GUI|Installation Destination|Filter", "%d _storage device selected", "%d _storage devices selected", count) % count label.set_text(summary) label.set_use_underline(True) summaryButton.set_visible(count > 0) label.set_sensitive(count > 0) def on_back_clicked(self, button): self.skipTo = "StorageSpoke" NormalSpoke.on_back_clicked(self, button) def on_summary_clicked(self, button): dialog = SelectedDisksDialog(self.data) # Include any disks selected in the initial storage spoke, plus any # selected in this filter UI. disks = [ disk for disk in self.disks if disk.name in self.selected_disks ] free_space = self.storage.get_free_space(disks=disks) with self.main_window.enlightbox(dialog.window): dialog.refresh(disks, free_space, showRemove=False, setBoot=False) dialog.run() @timed_action(delay=1200, busy_cursor=False) def on_filter_changed(self, *args): n = self._notebook.get_current_page() self.pages[n].filterActive = True self.pages[n].model.refilter() def on_clear_icon_clicked(self, entry, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: entry.set_text("") def on_page_switched(self, notebook, newPage, newPageNum, *args): self.pages[newPageNum].model.refilter() notebook.get_nth_page(newPageNum).show_all() def on_row_toggled(self, button, path): if not path: return page_index = self._notebook.get_current_page() filter_model = self.pages[page_index].model model_itr = filter_model.get_iter(path) itr = filter_model.convert_iter_to_child_iter(model_itr) self._store[itr][1] = not self._store[itr][1] if self._store[itr][1] and self._store[itr][ 3] not in self.selected_disks: self.selected_disks.append(self._store[itr][3]) elif not self._store[itr][1] and self._store[itr][ 3] in self.selected_disks: self.selected_disks.remove(self._store[itr][3]) self._update_summary() @timed_action(delay=50, threshold=100) def on_refresh_clicked(self, widget, *args): try_populate_devicetree(self.storage.devicetree) self.refresh() def on_add_iscsi_clicked(self, widget, *args): dialog = ISCSIDialog(self.data, self.storage) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() def on_add_fcoe_clicked(self, widget, *args): dialog = FCoEDialog(self.data, self.storage) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() def on_add_zfcp_clicked(self, widget, *args): dialog = ZFCPDialog(self.data, self.storage) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() def on_add_dasd_clicked(self, widget, *args): dialog = DASDDialog(self.data, self.storage) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() ## ## SEARCH TAB SIGNAL HANDLERS ## def on_search_type_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("searchTypeNotebook") notebook.set_current_page(ndx) self.on_filter_changed() ## ## MULTIPATH TAB SIGNAL HANDLERS ## def on_multipath_type_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("multipathTypeNotebook") notebook.set_current_page(ndx) self.on_filter_changed() ## ## OTHER TAB SIGNAL HANDLERS ## def on_other_type_combo_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("otherTypeNotebook") notebook.set_current_page(ndx) self.on_filter_changed() ## ## Z TAB SIGNAL HANDLERS ## def on_z_type_combo_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("zTypeNotebook") notebook.set_current_page(ndx) self.on_filter_changed()
class FilterSpoke(NormalSpoke): """ .. inheritance-diagram:: FilterSpoke :parts: 3 """ builderObjects = [ "diskStore", "filterWindow", "searchModel", "multipathModel", "otherModel", "zModel", "nvdimmModel" ] mainWidgetName = "filterWindow" uiFile = "spokes/advanced_storage.glade" helpFile = "FilterSpoke.xml" category = SystemCategory title = CN_("GUI|Spoke", "_Installation Destination") def __init__(self, *args): super().__init__(*args) self.applyOnSkip = True self._pages = {} self._ancestors = [] self._disks = [] self._selected_disks = [] self._disk_selection = STORAGE.get_proxy(DISK_SELECTION) self._notebook = self.builder.get_object("advancedNotebook") self._store = self.builder.get_object("diskStore") self._reconfigure_nvdimm_button = self.builder.get_object( "reconfigureNVDIMMButton") @property def indirect(self): return True # This spoke has no status since it's not in a hub @property def status(self): return None def apply(self): apply_disk_selection(self.storage, self._selected_disks) # some disks may have been added in this spoke, we need to recreate the # snapshot of on-disk storage if on_disk_storage.created: on_disk_storage.dispose_snapshot() on_disk_storage.create_snapshot(self.storage) def initialize(self): super().initialize() self.initialize_start() self._pages = { PAGE_SEARCH: SearchPage(self.storage, self.builder), PAGE_MULTIPATH: MultipathPage(self.storage, self.builder), PAGE_OTHER: OtherPage(self.storage, self.builder), PAGE_NVDIMM: NvdimmPage(self.storage, self.builder), PAGE_Z: ZPage(self.storage, self.builder), } if not STORAGE.get_proxy(DASD).IsSupported(): self._notebook.remove_page(PAGE_Z) self._pages.pop(PAGE_Z) self.builder.get_object("addZFCPButton").destroy() self.builder.get_object("addDASDButton").destroy() if not STORAGE.get_proxy(FCOE).IsSupported(): self.builder.get_object("addFCOEButton").destroy() if not STORAGE.get_proxy(ISCSI).IsSupported(): self.builder.get_object("addISCSIButton").destroy() # The button is sensitive only on NVDIMM page self._reconfigure_nvdimm_button.set_sensitive(False) # report that we are done self.initialize_done() def _real_ancestors(self, disk): # Return a list of all the ancestors of a disk, but remove the disk # itself from this list. return [d for d in disk.ancestors if d.name != disk.name] def refresh(self): super().refresh() self._disks = self.storage.usable_disks self._selected_disks = self._disk_selection.SelectedDisks self._ancestors = [ d.name for disk in self._disks for d in self._real_ancestors(disk) ] # Now all all the non-local disks to the store. Everything has been set up # ahead of time, so there's no need to configure anything. We first make # these lists of disks, then call setup on each individual page. This is # because there could be page-specific setup to do that requires a complete # view of all the disks on that page. self._store.clear() for page in self._pages.values(): page.setup( self._store, self._selected_disks, list(filter(page.is_member, self._disks)), ) self._update_summary() def _update_summary(self): summary_button = self.builder.get_object("summary_button") label = self.builder.get_object("summary_button_label") # We need to remove ancestor devices from the count. Otherwise, we'll # end up in a situation where selecting one multipath device could # potentially show three devices selected (mpatha, sda, sdb for instance). count = len([ disk for disk in self._selected_disks if disk not in self._ancestors ]) summary = CP_("GUI|Installation Destination|Filter", "{} _storage device selected", "{} _storage devices selected", count).format(count) label.set_text(summary) label.set_use_underline(True) summary_button.set_visible(count > 0) label.set_sensitive(count > 0) def on_back_clicked(self, button): self.skipTo = "StorageSpoke" super().on_back_clicked(button) def on_summary_clicked(self, button): disks = filter_disks_by_names(self._disks, self._selected_disks) dialog = SelectedDisksDialog(self.data, self.storage, disks, show_remove=False, set_boot=False) with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() @timed_action(delay=1200, busy_cursor=False) def on_filter_changed(self, *args): n = self._notebook.get_current_page() self._pages[n].is_active = True self._pages[n].model.refilter() def on_clear_icon_clicked(self, entry, icon_pos, event): if icon_pos == Gtk.EntryIconPosition.SECONDARY: entry.set_text("") def on_page_switched(self, notebook, new_page, new_page_num, *args): self._pages[new_page_num].model.refilter() notebook.get_nth_page(new_page_num).show_all() self._reconfigure_nvdimm_button.set_sensitive(new_page_num == 3) def on_row_toggled(self, button, path): if not path: return page_index = self._notebook.get_current_page() filter_model = self._pages[page_index].model model_itr = filter_model.get_iter(path) itr = filter_model.convert_iter_to_child_iter(model_itr) self._store[itr][1] = not self._store[itr][1] if self._store[itr][1] and self._store[itr][ 3] not in self._selected_disks: self._selected_disks.append(self._store[itr][3]) elif not self._store[itr][1] and self._store[itr][ 3] in self._selected_disks: self._selected_disks.remove(self._store[itr][3]) self._update_summary() @timed_action(delay=50, threshold=100) def on_refresh_clicked(self, widget, *args): try_populate_devicetree(self.storage.devicetree) self.refresh() def on_add_iscsi_clicked(self, widget, *args): dialog = ISCSIDialog(self.data, self.storage) self._run_dialog_and_refresh(dialog) def on_add_fcoe_clicked(self, widget, *args): dialog = FCoEDialog(self.data, self.storage) self._run_dialog_and_refresh(dialog) def on_add_zfcp_clicked(self, widget, *args): dialog = ZFCPDialog(self.data, self.storage) self._run_dialog_and_refresh(dialog) def on_add_dasd_clicked(self, widget, *args): dialog = DASDDialog(self.data, self.storage) self._run_dialog_and_refresh(dialog) def on_reconfigure_nvdimm_clicked(self, widget, *args): namespaces = self._pages[PAGE_NVDIMM].get_selected_namespaces() dialog = NVDIMMDialog(self.data, self.storage, namespaces) self._run_dialog_and_refresh(dialog) def _run_dialog_and_refresh(self, dialog): # Run the dialog. with self.main_window.enlightbox(dialog.window): dialog.refresh() dialog.run() # We now need to refresh so any new disks picked up by adding advanced # storage are displayed in the UI. self.refresh() def on_search_type_changed(self, combo): self._set_notebook_page("searchTypeNotebook", combo.get_active()) def on_multipath_type_changed(self, combo): self._set_notebook_page("multipathTypeNotebook", combo.get_active()) def on_other_type_combo_changed(self, combo): self._set_notebook_page("otherTypeNotebook", combo.get_active()) def on_nvdimm_type_combo_changed(self, combo): self._set_notebook_page("nvdimmTypeNotebook", combo.get_active()) def on_z_type_combo_changed(self, combo): self._set_notebook_page("zTypeNotebook", combo.get_active()) def _set_notebook_page(self, notebook_name, page_index): notebook = self.builder.get_object(notebook_name) notebook.set_current_page(page_index) self.on_filter_changed()
class SoftwareSelectionSpoke(NormalSpoke): """ .. inheritance-diagram:: SoftwareSelectionSpoke :parts: 3 """ builderObjects = ["addonStore", "environmentStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software_selection.glade" helpFile = "SoftwareSpoke.xml" category = SoftwareCategory icon = "package-x-generic-symbolic" title = CN_("GUI|Spoke", "_Software Selection") # Add-on selection states # no user interaction with this add-on _ADDON_DEFAULT = 0 # user selected _ADDON_SELECTED = 1 # user de-selected _ADDON_DESELECTED = 2 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._errorMsgs = None self._tx_id = None self._selectFlag = False self._environmentListBox = self.builder.get_object( "environmentListBox") self._addonListBox = self.builder.get_object("addonListBox") # Connect viewport scrolling with listbox focus events environmentViewport = self.builder.get_object("environmentViewport") addonViewport = self.builder.get_object("addonViewport") self._environmentListBox.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(environmentViewport)) self._addonListBox.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(addonViewport)) # Used to store how the user has interacted with add-ons for the default add-on # selection logic. The dictionary keys are group IDs, and the values are selection # state constants. See refreshAddons for how the values are used. self._addonStates = {} # Create a RadioButton that will never be displayed to use as the group for the # environment radio buttons. This way the environment radio buttons can all appear # unselected in the case of modifying data from kickstart. self._firstRadio = Gtk.RadioButton(group=None) # Used for detecting whether anything's changed in the spoke. self._origAddons = [] self._origEnvironment = None # Whether we are using package selections from a kickstart self._kickstarted = flags.automatedInstall and self.data.packages.seen # Whether the payload is in an error state self._error = False # Register event listeners to update our status on payload events payloadMgr.addListener(payloadMgr.STATE_PACKAGE_MD, self._downloading_package_md) payloadMgr.addListener(payloadMgr.STATE_GROUP_MD, self._downloading_group_md) payloadMgr.addListener(payloadMgr.STATE_FINISHED, self._payload_finished) payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error) # Add an invisible radio button so that we can show the environment # list with no radio buttons ticked self._fakeRadio = Gtk.RadioButton(group=None) self._fakeRadio.set_active(True) # Payload event handlers def _downloading_package_md(self): # Reset the error state from previous payloads self._error = False hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PACKAGE_MD)) def _downloading_group_md(self): hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_GROUP_MD)) @property def environment(self): """A wrapper for the environment specification in kickstart""" return self.data.packages.environment @environment.setter def environment(self, value): self.data.packages.environment = value @property def environmentid(self): """Return the "machine readable" environment id Alternatively we could have just "canonicalized" the environment description to the "machine readable" format when reading it from kickstart for the first time. But this could result in input and output kickstart, which would be rather confusing for the user. So we don't touch the specification from kickstart if it is valid and use this property when we need the "machine readable" form. """ if self.environment is None: # None means environment is not set, no need to try translate that to an id return None elif self.environment is False: # False means environment is not valid and must be set manually return False try: return self.payload.environmentId(self.environment) except NoSuchGroup: return None @property def environment_valid(self): """Return if the currently set environment is valid (represents an environment known by the payload) """ # None means the environment has not been set by the user, # which means: # * set the default environment during interactive installation # * ask user to specify an environment during kickstart installation if self.environment is None: return True else: return self.environmentid in self.payload.environments def _payload_finished(self): if self.environment_valid: log.info("using environment from kickstart: %s", self.environment) else: log.error( "unknown environment has been specified in kickstart and will be ignored: %s", self.data.packages.environment) # False means that the environment has been set to an invalid value and needs to # be manually set to a valid one. self.environment = False def _payload_error(self): self._error = True hubQ.send_message(self.__class__.__name__, payloadMgr.error) def _apply(self): # Environment needs to be set during a GUI installation, but is not required # for a kickstart install (even partial) if not self.environment: log.debug("Environment is not set, skip user packages settings") return # NOTE: This block is skipped for kickstart where addons and _origAddons will # both be [], preventing it from wiping out the kickstart's package selection addons = self._get_selected_addons() if not self._kickstarted and set(addons) != set(self._origAddons): self._selectFlag = False self.payload.data.packages.packageList = [] self.payload.data.packages.groupList = [] self.payload.selectEnvironment(self.environment) log.debug("Environment selected for installation: %s", self.environment) log.debug("Groups selected for installation: %s", addons) for group in addons: self.payload.selectGroup(group) # And then save these values so we can check next time. self._origAddons = addons self._origEnvironment = self.environment hubQ.send_not_ready(self.__class__.__name__) hubQ.send_not_ready("SourceSpoke") threadMgr.add( AnacondaThread(name=constants.THREAD_CHECK_SOFTWARE, target=self.checkSoftwareSelection)) def apply(self): self._apply() def checkSoftwareSelection(self): from pyanaconda.payload import DependencyError hubQ.send_message(self.__class__.__name__, _("Checking software dependencies...")) try: self.payload.checkSoftwareSelection() except DependencyError as e: self._errorMsgs = str(e) hubQ.send_message(self.__class__.__name__, _("Error checking software dependencies")) self._tx_id = None else: self._errorMsgs = None self._tx_id = self.payload.txID finally: hubQ.send_ready(self.__class__.__name__, False) hubQ.send_ready("SourceSpoke", False) @property def completed(self): processingDone = bool( not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and not threadMgr.get(constants.THREAD_PAYLOAD) and not self._errorMsgs and self.txid_valid) # * we should always check processingDone before checking the other variables, # as they might be inconsistent until processing is finished # * we can't let the installation proceed until a valid environment has been set if processingDone: if self.environment is not None: # if we have environment it needs to be valid return self.environment_valid # if we don't have environment we need to at least have the %packages # section in kickstart elif self._kickstarted: return True # no environment and no %packages section -> manual intervention is needed else: return False else: return False @property def changed(self): if not self.environment: return True addons = self._get_selected_addons() # Don't redo dep solving if nothing's changed. if self.environment == self._origEnvironment and set(addons) == set(self._origAddons) and \ self.txid_valid: return False return True @property def mandatory(self): return True @property def ready(self): # By default, the software selection spoke is not ready. We have to # wait until the installation source spoke is completed. This could be # because the user filled something out, or because we're done fetching # repo metadata from the mirror list, or we detected a DVD/CD. return bool(not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and not threadMgr.get(constants.THREAD_PAYLOAD) and not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and self.payload.baseRepo is not None) @property def showable(self): return isinstance(self.payload, PackagePayload) @property def status(self): if self._errorMsgs: return _("Error checking software selection") if not self.ready: return _("Installation source not set up") if not self.txid_valid: return _("Source changed - please verify") # kickstart installation if flags.automatedInstall: if self._kickstarted: # %packages section is present in kickstart but environment is not set if self.environment is None: return _("Custom software selected") # environment is set to an invalid value elif not self.environment_valid: return _("Invalid environment specified in kickstart") # we have no packages section in the kickstart and no environment has been set elif not self.environment: return _("Nothing selected") if not flags.automatedInstall: if not self.environment: # No environment yet set return _("Nothing selected") elif not self.environment_valid: # selected environment is not valid, this can happen when a valid environment # is selected (by default, manually or from kickstart) and then the installation # source is switched to one where the selected environment is no longer valid return _("Selected environment is not valid") return self.payload.environmentDescription(self.environment)[0] def initialize(self): super().initialize() self.initialize_start() threadMgr.add( AnacondaThread(name=constants.THREAD_SOFTWARE_WATCHER, target=self._initialize)) def _initialize(self): threadMgr.wait(constants.THREAD_PAYLOAD) # Select groups which should be selected by kickstart try: for group in self.payload.selectedGroupsIDs(): if self.environment and self.payload.environmentOptionIsDefault( self.environment, group): self._addonStates[group] = self._ADDON_DEFAULT else: self._addonStates[group] = self._ADDON_SELECTED except PayloadError as e: # Group translation is not supported log.warning(e) # It's better to have all or nothing selected from kickstart self._addonStates = {} if not self._kickstarted: # having done all the slow downloading, we need to do the first refresh # of the UI here so there's an environment selected by default. This # happens inside the main thread by necessity. We can't do anything # that takes any real amount of time, or it'll block the UI from # updating. if not self._first_refresh(): return hubQ.send_ready(self.__class__.__name__, False) # If packages were provided by an input kickstart file (or some other means), # we should do dependency solving here. if not self._error: self._apply() # report that software spoke initialization has been completed self.initialize_done() @async_action_wait def _first_refresh(self): self.refresh() return True def _add_row(self, listbox, name, desc, button, clicked): row = Gtk.ListBoxRow() box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) button.set_valign(Gtk.Align.START) button.connect("toggled", clicked, row) box.add(button) label = Gtk.Label(label="<b>%s</b>\n%s" % (escape_markup(name), escape_markup(desc)), use_markup=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=True, xalign=0, yalign=0.5) box.add(label) row.add(box) listbox.insert(row, -1) def refresh(self): super().refresh() threadMgr.wait(constants.THREAD_PAYLOAD) firstEnvironment = True self._clear_listbox(self._environmentListBox) # If no environment is selected, use the default from the instclass. # If nothing is set in the instclass, the first environment will be # selected below. if not self.environment and self.payload.instclass and \ self.payload.instclass.defaultPackageEnvironment in self.payload.environments: self.environment = self.payload.instclass.defaultPackageEnvironment # create rows for all valid environments for environmentid in self.payload.environments: (name, desc) = self.payload.environmentDescription(environmentid) # use the invisible radio button as a group for all environment # radio buttons radio = Gtk.RadioButton(group=self._fakeRadio) # automatically select the first environment if we are on # manual install and the install class does not specify one if firstEnvironment and not flags.automatedInstall: # manual installation # # Note about self.environment being None: # ======================================= # None indicates that an environment has not been set, which is a valid # value of the environment variable. # Only non existing environments are evaluated as invalid if not self.environment_valid or self.environment is None: self.environment = environmentid firstEnvironment = False # check if the selected environment (if any) does match the current row # and tick the radio button if it does radio.set_active(self.environment_valid and self.environmentid == environmentid) self._add_row(self._environmentListBox, name, desc, radio, self.on_radio_button_toggled) self.refreshAddons() self._environmentListBox.show_all() self._addonListBox.show_all() def _addAddon(self, grp): (name, desc) = self.payload.groupDescription(grp) if grp in self._addonStates: # If the add-on was previously selected by the user, select it if self._addonStates[grp] == self._ADDON_SELECTED: selected = True # If the add-on was previously de-selected by the user, de-select it elif self._addonStates[grp] == self._ADDON_DESELECTED: selected = False # Otherwise, use the default state else: selected = self.payload.environmentOptionIsDefault( self.environmentid, grp) else: selected = self.payload.environmentOptionIsDefault( self.environmentid, grp) check = Gtk.CheckButton() check.set_active(selected) self._add_row(self._addonListBox, name, desc, check, self.on_checkbox_toggled) @property def _addSep(self): """ Whether the addon list contains a separator. """ return len(self.payload.environmentAddons[self.environmentid][0]) > 0 and \ len(self.payload.environmentAddons[self.environmentid][1]) > 0 def refreshAddons(self): if self.environment and (self.environmentid in self.payload.environmentAddons): self._clear_listbox(self._addonListBox) # We have two lists: One of addons specific to this environment, # and one of all the others. The environment-specific ones will be displayed # first and then a separator, and then the generic ones. This is to make it # a little more obvious that the thing on the left side of the screen and the # thing on the right side of the screen are related. # # If a particular add-on was previously selected or de-selected by the user, that # state will be used. Otherwise, the add-on will be selected if it is a default # for this environment. for grp in self.payload.environmentAddons[self.environmentid][0]: self._addAddon(grp) # This marks a separator in the view - only add it if there's both environment # specific and generic addons. if self._addSep: self._addonListBox.insert(Gtk.Separator(), -1) for grp in self.payload.environmentAddons[self.environmentid][1]: self._addAddon(grp) self._selectFlag = True if self._errorMsgs: self.set_warning( _("Error checking software dependencies. <a href=\"\">Click for details.</a>" )) else: self.clear_info() def _allAddons(self): if self.environmentid in self.payload.environmentAddons: addons = copy.copy( self.payload.environmentAddons[self.environmentid][0]) if self._addSep: addons.append('') addons += self.payload.environmentAddons[self.environmentid][1] else: addons = [] return addons def _get_selected_addons(self): retval = [] addons = self._allAddons() for (ndx, row) in enumerate(self._addonListBox.get_children()): box = row.get_children()[0] if isinstance(box, Gtk.Separator): continue button = box.get_children()[0] if button.get_active(): retval.append(addons[ndx]) return retval def _mark_addon_selection(self, grpid, selected): # Mark selection or return its state to the default state if selected: if self.payload.environmentOptionIsDefault(self.environment, grpid): self._addonStates[grpid] = self._ADDON_DEFAULT else: self._addonStates[grpid] = self._ADDON_SELECTED else: if not self.payload.environmentOptionIsDefault( self.environment, grpid): self._addonStates[grpid] = self._ADDON_DEFAULT else: self._addonStates[grpid] = self._ADDON_DESELECTED def _clear_listbox(self, listbox): for child in listbox.get_children(): listbox.remove(child) del (child) @property def txid_valid(self): return self._tx_id == self.payload.txID # Signal handlers def on_radio_button_toggled(self, radio, row): # If the radio button toggled to inactive, don't reactivate the row if not radio.get_active(): return row.activate() def on_environment_activated(self, listbox, row): if not self._selectFlag: return # GUI selections means that packages are no longer coming from kickstart self._kickstarted = False box = row.get_children()[0] button = box.get_children()[0] with blockedHandler(button, self.on_radio_button_toggled): button.set_active(True) # Mark the clicked environment as selected and update the screen. self.environment = self.payload.environments[row.get_index()] self.refreshAddons() self._addonListBox.show_all() def on_checkbox_toggled(self, button, row): # Select the addon. The button is already toggled. self._select_addon_at_row(row, button.get_active()) def on_addon_activated(self, listbox, row): # Skip the separator. box = row.get_children()[0] if isinstance(box, Gtk.Separator): return # Select the addon. The button is not toggled yet. button = box.get_children()[0] self._select_addon_at_row(row, not button.get_active()) def _select_addon_at_row(self, row, is_selected): # GUI selections means that packages are no longer coming from kickstart. self._kickstarted = False # Activate the row. with blockedHandler(row.get_parent(), self.on_addon_activated): row.activate() # Activate the button. box = row.get_children()[0] button = box.get_children()[0] with blockedHandler(button, self.on_checkbox_toggled): button.set_active(is_selected) # Mark the selection. addons = self._allAddons() group = addons[row.get_index()] self._mark_addon_selection(group, is_selected) def on_info_bar_clicked(self, *args): if not self._errorMsgs: return label = _( "The software marked for installation has the following errors. " "This is likely caused by an error with your installation source. " "You can quit the installer, change your software source, or change " "your software selections.") dialog = DetailedErrorDialog( self.data, buttons=[ C_("GUI|Software Selection|Error Dialog", "_Quit"), C_("GUI|Software Selection|Error Dialog", "_Modify Software Source"), C_("GUI|Software Selection|Error Dialog", "Modify _Selections") ], label=label) with self.main_window.enlightbox(dialog.window): dialog.refresh(self._errorMsgs) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. util.ipmi_abort(scripts=self.data.scripts) sys.exit(0) elif rc == 1: # Send the user to the installation source spoke. self.skipTo = "SourceSpoke" self.window.emit("button-clicked") elif rc == 2: # Close the dialog so the user can change selections. pass else: pass
class BlivetGuiSpoke(NormalSpoke, StorageCheckHandler): ### class attributes defined by API ### # list all top-level objects from the .glade file that should be exposed # to the spoke or leave empty to extract everything builderObjects = ["blivetGuiSpokeWindow"] # the name of the main window widget mainWidgetName = "blivetGuiSpokeWindow" # name of the .glade file in the same directory as this source uiFile = "spokes/blivet_gui.glade" # category this spoke belongs to category = SystemCategory # title of the spoke (will be displayed on the hub) title = CN_("GUI|Spoke", "_Blivet-GUI Partitioning") helpFile = "blivet-gui/index.page" ### methods defined by API ### def __init__(self, data, storage, payload): """ :see: pyanaconda.ui.common.Spoke.__init__ :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing payload-related information :type payload: pyanaconda.payload.Payload """ self._error = None self._back_already_clicked = False self._label_actions = None self._button_reset = None self._button_undo = None self._client = None self._blivetgui = None self._partitioning = None self._device_tree = None self._storage_module = STORAGE.get_proxy() StorageCheckHandler.__init__(self) NormalSpoke.__init__(self, data, storage, payload) @property def label_actions(self): """The summary label. This property is required by Blivet-GUI. """ return self._label_actions def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalSpoke.initialize(self) self.initialize_start() config.log_dir = "/tmp" box = self.builder.get_object("BlivetGuiViewport") self._label_actions = self.builder.get_object("summary_label") self._button_reset = self.builder.get_object("resetAllButton") self._button_undo = self.builder.get_object("undoLastActionButton") self._client = BlivetGUIAnacondaClient() self._blivetgui = BlivetGUIAnaconda(self._client, self, box) # this needs to be done when the spoke is already "realized" self.entered.connect(self._blivetgui.ui_refresh) # set up keyboard shurtcuts for blivet-gui (and unset them after # user lefts the spoke) self.entered.connect(self._blivetgui.set_keyboard_shortcuts) self.exited.connect(self._blivetgui.unset_keyboard_shortcuts) self.initialize_done() def refresh(self): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh """ for thread_name in [THREAD_EXECUTE_STORAGE, THREAD_STORAGE]: threadMgr.wait(thread_name) if not self._partitioning: # Create the partitioning now. It cannot by done earlier, because # the storage spoke would use it as a default partitioning. self._partitioning = create_partitioning( PARTITIONING_METHOD_BLIVET) self._device_tree = STORAGE.get_proxy( self._partitioning.GetDeviceTree()) self._back_already_clicked = False self._client.initialize(self._partitioning.SendRequest) self._blivetgui.initialize() # if we re-enter blivet-gui spoke, actions from previous visit were # not removed, we need to update number of blivet-gui actions self._blivetgui.set_actions(self._client.get_actions()) def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the GUI elements. """ pass @property def indirect(self): return True # This spoke has no status since it's not in a hub @property def status(self): return None def clear_errors(self): self._error = None self.clear_info() def _do_check(self): self.clear_errors() StorageCheckHandler.errors = [] StorageCheckHandler.warnings = [] try: log.debug("Generating updated storage configuration") task_path = self._partitioning.ConfigureWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) except BootloaderConfigurationError as e: log.error("Storage configuration failed: %s", e) StorageCheckHandler.errors = [str(e)] reset_bootloader() else: log.debug("Checking storage configuration...") task_path = self._partitioning.ValidateWithTask() task_proxy = STORAGE.get_proxy(task_path) sync_run_task(task_proxy) result = unwrap_variant(task_proxy.GetResult()) report = ValidationReport.from_structure(result) log.debug("Validation has been completed: %s", report) StorageCheckHandler.errors = report.error_messages StorageCheckHandler.warnings = report.warning_messages if report.is_valid(): self._storage_module.ApplyPartitioning( get_object_path(self._partitioning)) if self.errors: self.set_warning( _("Error checking storage configuration. <a href=\"\">Click for details</a> or press Done again to continue." )) elif self.warnings: self.set_warning( _("Warning checking storage configuration. <a href=\"\">Click for details</a> or press Done again to continue." )) # on_info_bar_clicked requires self._error to be set, so set it to the # list of all errors and warnings that storage checking found. self._error = "\n".join(self.errors + self.warnings) return self._error == "" def activate_action_buttons(self, activate): self._button_undo.set_sensitive(activate) self._button_reset.set_sensitive(activate) ### handlers ### def on_info_bar_clicked(self, *args): log.debug("info bar clicked: %s (%s)", self._error, args) if not self._error: return dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.CLOSE, message_format=str(self._error)) dlg.set_decorated(False) with self.main_window.enlightbox(dlg): dlg.run() dlg.destroy() def on_back_clicked(self, button): # Clear any existing errors self.clear_errors() # If back has been clicked on once already and no other changes made on the screen, # run the storage check now. This handles displaying any errors in the info bar. if not self._back_already_clicked: self._back_already_clicked = True # If we hit any errors while saving things above, stop and let the # user think about what they have done if self._error is not None: return if not self._do_check(): return dialog = ActionSummaryDialog(self.data, self._device_tree) dialog.refresh() if dialog.actions: with self.main_window.enlightbox(dialog.window): rc = dialog.run() if rc != 1: # Cancel. Stay on the blivet-gui screen. return NormalSpoke.on_back_clicked(self, button) def on_summary_button_clicked(self, _button): self._blivetgui.show_actions() def on_undo_action_button_clicked(self, _button): self._blivetgui.actions_undo() # This callback is for the button that just resets the UI to anaconda's # current understanding of the disk layout. def on_reset_button_clicked(self, *args): msg = _( "Continuing with this action will reset all your partitioning selections " "to their current on-disk state.") dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.NONE, message_format=msg) dlg.set_decorated(False) dlg.add_buttons( C_("GUI|Custom Partitioning|Reset Dialog", "_Reset selections"), 0, C_("GUI|Custom Partitioning|Reset Dialog", "_Preserve current selections"), 1) dlg.set_default_response(1) with self.main_window.enlightbox(dlg): rc = dlg.run() dlg.destroy() if rc == 0: self.refresh() self._blivetgui.reload() # XXX: Reset currently preserves actions set in previous runs # of the spoke, so we need to 're-add' these to the ui self._blivetgui.set_actions(self._client.get_actions())
class SoftwareSelectionSpoke(NormalSpoke): """ .. inheritance-diagram:: SoftwareSelectionSpoke :parts: 3 """ builderObjects = ["addonStore", "environmentStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software_selection.glade" helpFile = "SoftwareSpoke.xml" category = SoftwareCategory icon = "package-x-generic-symbolic" title = CN_("GUI|Spoke", "_Software Selection") # Add-on selection states # no user interaction with this add-on _ADDON_DEFAULT = 0 # user selected _ADDON_SELECTED = 1 # user de-selected _ADDON_DESELECTED = 2 @classmethod def should_run(cls, environment, data): """Don't run for any non-package payload.""" if not NormalSpoke.should_run(environment, data): return False return context.payload_type == PAYLOAD_TYPE_DNF def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._error_msgs = None self._tx_id = None self._select_flag = False self._environment_list_box = self.builder.get_object("environmentListBox") self._addon_list_box = self.builder.get_object("addonListBox") # Connect viewport scrolling with listbox focus events environment_viewport = self.builder.get_object("environmentViewport") addon_viewport = self.builder.get_object("addonViewport") self._environment_list_box.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(environment_viewport)) self._addon_list_box.set_focus_vadjustment( Gtk.Scrollable.get_vadjustment(addon_viewport)) # Used to store how the user has interacted with add-ons for the default add-on # selection logic. The dictionary keys are group IDs, and the values are selection # state constants. See refresh_addons for how the values are used. self._addon_states = {} # Get the packages configuration. self._selection = self.payload.get_packages_data() # Whether we are using package selections from a kickstart self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted # Whether the payload is in an error state self._error = False # Register event listeners to update our status on payload events payloadMgr.add_listener(PayloadState.DOWNLOADING_PKG_METADATA, self._downloading_package_md) payloadMgr.add_listener(PayloadState.DOWNLOADING_GROUP_METADATA, self._downloading_group_md) payloadMgr.add_listener(PayloadState.ERROR, self._payload_error) # Add an invisible radio button so that we can show the environment # list with no radio buttons ticked self._fake_radio = Gtk.RadioButton(group=None) self._fake_radio.set_active(True) # Payload event handlers def _downloading_package_md(self): # Reset the error state from previous payloads self._error = False hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PACKAGE_MD)) def _downloading_group_md(self): hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_GROUP_MD)) def get_environment_id(self, environment): """Return the "machine readable" environment id Alternatively we could have just "canonicalized" the environment description to the "machine readable" format when reading it from kickstart for the first time. But this could result in input and output kickstart, which would be rather confusing for the user. So we don't touch the specification from kickstart if it is valid and use this property when we need the "machine readable" form. """ if not environment: # None means environment is not set, no need to try translate that to an id return None try: return self.payload.environment_id(environment) except NoSuchGroup: return None def is_environment_valid(self, environment): """Return if the currently set environment is valid (represents an environment known by the payload) """ # None means the environment has not been set by the user, # which means: # * set the default environment during interactive installation # * ask user to specify an environment during kickstart installation if not environment: return True else: return self.get_environment_id(environment) in self.payload.environments def _payload_error(self): self._error = True hubQ.send_message(self.__class__.__name__, payloadMgr.error) def apply(self): """Apply the changes.""" self._kickstarted = False # Clear packages data. self._selection.packages = [] self._selection.excluded_packages = [] # Clear groups data. self._selection.excluded_groups = [] self._selection.groups_package_types = {} # Select new groups. self._selection.groups = [] for group_name in self._get_selected_addons(): self._selection.groups.append(group_name) log.debug("Setting new software selection: %s", self._selection) self.payload.set_packages_data(self._selection) hubQ.send_not_ready(self.__class__.__name__) hubQ.send_not_ready("SourceSpoke") def execute(self): """Execute the changes.""" threadMgr.add(AnacondaThread( name=constants.THREAD_CHECK_SOFTWARE, target=self.checkSoftwareSelection )) def checkSoftwareSelection(self): hubQ.send_message(self.__class__.__name__, _("Checking software dependencies...")) try: self.payload.check_software_selection() except DependencyError as e: self._error_msgs = str(e) hubQ.send_message(self.__class__.__name__, _("Error checking software dependencies")) self._tx_id = None else: self._error_msgs = None self._tx_id = self.payload.tx_id finally: hubQ.send_ready(self.__class__.__name__) hubQ.send_ready("SourceSpoke") @property def completed(self): processing_done = bool(not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and not threadMgr.get(constants.THREAD_PAYLOAD) and not self._error_msgs and self.txid_valid) # * we should always check processing_done before checking the other variables, # as they might be inconsistent until processing is finished # * we can't let the installation proceed until a valid environment has been set if processing_done: if self._selection.environment: # if we have environment it needs to be valid return self.is_environment_valid(self._selection.environment) # if we don't have environment we need to at least have the %packages # section in kickstart elif self._kickstarted: return True # no environment and no %packages section -> manual intervention is needed else: return False else: return False @property def mandatory(self): return True @property def ready(self): """Is the spoke ready? By default, the software selection spoke is not ready. We have to wait until the installation source spoke is completed. This could be because the user filled something out, or because we're done fetching repo metadata from the mirror list, or we detected a DVD/CD. """ return not threadMgr.get(THREAD_SOFTWARE_WATCHER) \ and not threadMgr.get(THREAD_PAYLOAD) \ and not threadMgr.get(THREAD_CHECK_SOFTWARE) \ and self.payload.base_repo is not None @property def status(self): if self._error_msgs: return _("Error checking software selection") cdn_source = check_cdn_is_installation_source(self.payload) subscribed = False if is_module_available(SUBSCRIPTION): subscription_proxy = SUBSCRIPTION.get_proxy() subscribed = subscription_proxy.IsSubscriptionAttached if cdn_source and not subscribed: return _("Red Hat CDN requires registration.") if not self.ready: return _("Installation source not set up") if not self.txid_valid: return _("Source changed - please verify") # kickstart installation if flags.automatedInstall: if self._kickstarted: # %packages section is present in kickstart but environment is not set if not self._selection.environment: return _("Custom software selected") # environment is set to an invalid value elif not self.is_environment_valid(self._selection.environment): return _("Invalid environment specified in kickstart") # we have no packages section in the kickstart and no environment has been set elif not self._selection.environment: return _("Please confirm software selection") if not flags.automatedInstall: if not self._selection.environment: # No environment yet set return _("Please confirm software selection") elif not self.is_environment_valid(self._selection.environment): # selected environment is not valid, this can happen when a valid environment # is selected (by default, manually or from kickstart) and then the installation # source is switched to one where the selected environment is no longer valid return _("Selected environment is not valid") return self.payload.environment_description(self._selection.environment)[0] def initialize(self): """Initialize the spoke.""" super().initialize() self.initialize_start() threadMgr.add(AnacondaThread( name=constants.THREAD_SOFTWARE_WATCHER, target=self._initialize )) def _initialize(self): """Initialize the spoke in a separate thread.""" threadMgr.wait(constants.THREAD_PAYLOAD) # Initialize and check the software selection. self._initialize_selection() # Update the status. hubQ.send_ready(self.__class__.__name__) # Report that the software spoke has been initialized. self.initialize_done() def _initialize_selection(self): """Initialize and check the software selection.""" if self._error or not self.payload.base_repo: log.debug("Skip the initialization of the software selection.") return if not self._kickstarted: # Set the environment. self.set_default_environment() # Apply the initial selection. self.apply() # Check the initial software selection. self.execute() # Wait for the software selection thread that might be started by execute(). # We are already running in a thread, so it should not needlessly block anything # and only like this we can be sure we are really initialized. threadMgr.wait(constants.THREAD_CHECK_SOFTWARE) def set_default_environment(self): # If an environment was specified in the configuration, use that. # Otherwise, select the first environment. if self.payload.environments: environments = self.payload.environments if conf.payload.default_environment in environments: self._selection.environment = conf.payload.default_environment else: self._selection.environment = environments[0] def _add_row(self, listbox, name, desc, button, clicked): row = Gtk.ListBoxRow() box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) button.set_valign(Gtk.Align.START) button.connect("toggled", clicked, row) box.add(button) label = Gtk.Label(label="<b>%s</b>\n%s" % (escape_markup(name), escape_markup(desc)), use_markup=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=True, xalign=0, yalign=0.5) box.add(label) row.add(box) listbox.insert(row, -1) def refresh(self): super().refresh() threadMgr.wait(constants.THREAD_PAYLOAD) # Get the packages configuration. self._selection = self.payload.get_packages_data() # Set up the environment. if not self._selection.environment \ or not self.is_environment_valid(self._selection.environment): self.set_default_environment() # Create rows for all valid environments. self._clear_listbox(self._environment_list_box) for environment_id in self.payload.environments: (name, desc) = self.payload.environment_description(environment_id) # use the invisible radio button as a group for all environment # radio buttons radio = Gtk.RadioButton(group=self._fake_radio) # check if the selected environment (if any) does match the current row # and tick the radio button if it does radio.set_active( self.is_environment_valid(self._selection.environment) and self.get_environment_id(self._selection.environment) == environment_id ) self._add_row(self._environment_list_box, name, desc, radio, self.on_radio_button_toggled) # Set up states of selected groups. self._addon_states = {} for group in self._selection.groups: try: group_id = self.payload.group_id(group) self._mark_addon_selection(group_id, True) except PayloadError as e: log.warning(e) self.refresh_addons() self._environment_list_box.show_all() self._addon_list_box.show_all() def _add_addon(self, grp): (name, desc) = self.payload.group_description(grp) if grp in self._addon_states: # If the add-on was previously selected by the user, select it if self._addon_states[grp] == self._ADDON_SELECTED: selected = True # If the add-on was previously de-selected by the user, de-select it elif self._addon_states[grp] == self._ADDON_DESELECTED: selected = False # Otherwise, use the default state else: selected = self.payload.environment_option_is_default( self.get_environment_id(self._selection.environment), grp ) else: selected = self.payload.environment_option_is_default( self.get_environment_id(self._selection.environment), grp ) check = Gtk.CheckButton() check.set_active(selected) self._add_row(self._addon_list_box, name, desc, check, self.on_checkbox_toggled) @property def _add_sep(self): """ Whether the addon list contains a separator. """ environment_id = self.get_environment_id(self._selection.environment) return len(self.payload.environment_addons[environment_id][0]) > 0 and \ len(self.payload.environment_addons[environment_id][1]) > 0 def refresh_addons(self): environment = self._selection.environment environment_id = self.get_environment_id(self._selection.environment) if environment and (environment_id in self.payload.environment_addons): self._clear_listbox(self._addon_list_box) # We have two lists: One of addons specific to this environment, # and one of all the others. The environment-specific ones will be displayed # first and then a separator, and then the generic ones. This is to make it # a little more obvious that the thing on the left side of the screen and the # thing on the right side of the screen are related. # # If a particular add-on was previously selected or de-selected by the user, that # state will be used. Otherwise, the add-on will be selected if it is a default # for this environment. for grp in self.payload.environment_addons[environment_id][0]: self._add_addon(grp) # This marks a separator in the view - only add it if there's both environment # specific and generic addons. if self._add_sep: self._addon_list_box.insert(Gtk.Separator(), -1) for grp in self.payload.environment_addons[environment_id][1]: self._add_addon(grp) self._select_flag = True if self._error_msgs: self.set_warning(_("Error checking software dependencies. " " <a href=\"\">Click for details.</a>")) else: self.clear_info() def _all_addons(self): environment_id = self.get_environment_id(self._selection.environment) if environment_id in self.payload.environment_addons: addons = copy.copy(self.payload.environment_addons[environment_id][0]) if self._add_sep: addons.append('') addons += self.payload.environment_addons[environment_id][1] else: addons = [] return addons def _get_selected_addons(self): retval = [] addons = self._all_addons() for (ndx, row) in enumerate(self._addon_list_box.get_children()): box = row.get_children()[0] if isinstance(box, Gtk.Separator): continue button = box.get_children()[0] if button.get_active(): retval.append(addons[ndx]) return retval def _mark_addon_selection(self, grpid, selected): # Mark selection or return its state to the default state if selected: if self.payload.environment_option_is_default(self._selection.environment, grpid): self._addon_states[grpid] = self._ADDON_DEFAULT else: self._addon_states[grpid] = self._ADDON_SELECTED else: if not self.payload.environment_option_is_default(self._selection.environment, grpid): self._addon_states[grpid] = self._ADDON_DEFAULT else: self._addon_states[grpid] = self._ADDON_DESELECTED def _clear_listbox(self, listbox): for child in listbox.get_children(): listbox.remove(child) del(child) @property def txid_valid(self): return self._tx_id == self.payload.tx_id # Signal handlers def on_radio_button_toggled(self, radio, row): # If the radio button toggled to inactive, don't reactivate the row if not radio.get_active(): return row.activate() def on_environment_activated(self, listbox, row): if not self._select_flag: return # GUI selections means that packages are no longer coming from kickstart self._kickstarted = False box = row.get_children()[0] button = box.get_children()[0] with blockedHandler(button, self.on_radio_button_toggled): button.set_active(True) # Mark the clicked environment as selected and update the screen. self._selection.environment = self.payload.environments[row.get_index()] self.refresh_addons() self._addon_list_box.show_all() def on_checkbox_toggled(self, button, row): # Select the addon. The button is already toggled. self._select_addon_at_row(row, button.get_active()) def on_addon_activated(self, listbox, row): # Skip the separator. box = row.get_children()[0] if isinstance(box, Gtk.Separator): return # Select the addon. The button is not toggled yet. button = box.get_children()[0] self._select_addon_at_row(row, not button.get_active()) def _select_addon_at_row(self, row, is_selected): # GUI selections means that packages are no longer coming from kickstart. self._kickstarted = False # Activate the row. with blockedHandler(row.get_parent(), self.on_addon_activated): row.activate() # Activate the button. box = row.get_children()[0] button = box.get_children()[0] with blockedHandler(button, self.on_checkbox_toggled): button.set_active(is_selected) # Mark the selection. addons = self._all_addons() group = addons[row.get_index()] self._mark_addon_selection(group, is_selected) def on_info_bar_clicked(self, *args): if not self._error_msgs: return label = _("The software marked for installation has the following errors. " "This is likely caused by an error with your installation source. " "You can quit the installer, change your software source, or change " "your software selections.") dialog = DetailedErrorDialog( self.data, buttons=[C_("GUI|Software Selection|Error Dialog", "_Quit"), C_("GUI|Software Selection|Error Dialog", "_Modify Software Source"), C_("GUI|Software Selection|Error Dialog", "Modify _Selections")], label=label) with self.main_window.enlightbox(dialog.window): dialog.refresh(self._error_msgs) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. util.ipmi_abort(scripts=self.data.scripts) sys.exit(0) elif rc == 1: # Send the user to the installation source spoke. self.skipTo = "SourceSpoke" self.window.emit("button-clicked") elif rc == 2: # Close the dialog so the user can change selections. pass else: pass
class BlivetGuiSpoke(NormalSpoke, StorageCheckHandler): ### class attributes defined by API ### # list all top-level objects from the .glade file that should be exposed # to the spoke or leave empty to extract everything builderObjects = ["blivetGuiSpokeWindow"] # the name of the main window widget mainWidgetName = "blivetGuiSpokeWindow" # name of the .glade file in the same directory as this source uiFile = "spokes/blivet_gui.glade" # category this spoke belongs to category = SystemCategory # title of the spoke (will be displayed on the hub) title = CN_("GUI|Spoke", "_Blivet-GUI Partitioning") helpFile = "blivet-gui/index.page" ### methods defined by API ### def __init__(self, data, storage, payload, instclass): """ :see: pyanaconda.ui.common.Spoke.__init__ :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing payload-related information :type payload: pyanaconda.payload.Payload :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass """ self._error = None self._back_already_clicked = False self._storage_playground = None self.label_actions = None self.button_reset = None self.button_undo = None self._bootloader_observer = STORAGE.get_observer(BOOTLOADER) self._bootloader_observer.connect() StorageCheckHandler.__init__(self) NormalSpoke.__init__(self, data, storage, payload, instclass) def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalSpoke.initialize(self) self.initialize_start() self._storage_playground = None config.log_dir = "/tmp" self.client = osinstall.BlivetGUIAnacondaClient() box = self.builder.get_object("BlivetGuiViewport") self.label_actions = self.builder.get_object("summary_label") self.button_reset = self.builder.get_object("resetAllButton") self.button_undo = self.builder.get_object("undoLastActionButton") config.default_fstype = self._storage.default_fstype self.blivetgui = osinstall.BlivetGUIAnaconda(self.client, self, box) # this needs to be done when the spoke is already "realized" self.entered.connect(self.blivetgui.ui_refresh) # set up keyboard shurtcuts for blivet-gui (and unset them after # user lefts the spoke) self.entered.connect(self.blivetgui.set_keyboard_shortcuts) self.exited.connect(self.blivetgui.unset_keyboard_shortcuts) self.initialize_done() def refresh(self): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh """ self._back_already_clicked = False self._storage_playground = self.storage.copy() self.client.initialize(self._storage_playground) self.blivetgui.initialize() # if we re-enter blivet-gui spoke, actions from previous visit were # not removed, we need to update number of blivet-gui actions current_actions = self._storage_playground.devicetree.actions.find() if current_actions: self.blivetgui.set_actions(current_actions) def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the GUI elements. """ self._set_new_swaps() @property def indirect(self): return True # This spoke has no status since it's not in a hub @property def status(self): return None def clear_errors(self): self._error = None self.clear_info() def _do_check(self): self.clear_errors() StorageCheckHandler.errors = [] StorageCheckHandler.warnings = [] # We can't overwrite the main Storage instance because all the other # spokes have references to it that would get invalidated, but we can # achieve the same effect by updating/replacing a few key attributes. self.storage.devicetree._devices = self._storage_playground.devicetree._devices self.storage.devicetree._actions = self._storage_playground.devicetree._actions self.storage.devicetree._hidden = self._storage_playground.devicetree._hidden self.storage.devicetree.names = self._storage_playground.devicetree.names self.storage.roots = self._storage_playground.roots # set up bootloader and check the configuration try: self.storage.set_up_bootloader() except BootLoaderError as e: log.error("storage configuration failed: %s", e) StorageCheckHandler.errors = str(e).split("\n") self._bootloader_observer.proxy.SetDrive(BOOTLOADER_DRIVE_UNSET) StorageCheckHandler.checkStorage(self) if self.errors: self.set_warning( _("Error checking storage configuration. <a href=\"\">Click for details</a> or press Done again to continue." )) elif self.warnings: self.set_warning( _("Warning checking storage configuration. <a href=\"\">Click for details</a> or press Done again to continue." )) # on_info_bar_clicked requires self._error to be set, so set it to the # list of all errors and warnings that storage checking found. self._error = "\n".join(self.errors + self.warnings) return self._error == "" def activate_action_buttons(self, activate): self.button_undo.set_sensitive(activate) self.button_reset.set_sensitive(activate) def _set_new_swaps(self): new_swaps = [ d for d in self._storage_playground.devices if d.direct and not d.format.exists and not d.partitioned and d.format.type == "swap" ] self.storage.set_fstab_swaps(new_swaps) ### handlers ### def on_info_bar_clicked(self, *args): log.debug("info bar clicked: %s (%s)", self._error, args) if not self._error: return dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.CLOSE, message_format=str(self._error)) dlg.set_decorated(False) with self.main_window.enlightbox(dlg): dlg.run() dlg.destroy() def on_back_clicked(self, button): # Clear any existing errors self.clear_errors() # If back has been clicked on once already and no other changes made on the screen, # run the storage check now. This handles displaying any errors in the info bar. if not self._back_already_clicked: self._back_already_clicked = True # If we hit any errors while saving things above, stop and let the # user think about what they have done if self._error is not None: return if not self._do_check(): return if len(self._storage_playground.devicetree.actions.find()) > 0: dialog = ActionSummaryDialog(self.data) dialog.refresh(self._storage_playground.devicetree.actions.find()) with self.main_window.enlightbox(dialog.window): rc = dialog.run() if rc != 1: # Cancel. Stay on the blivet-gui screen. return else: # remove redundant actions and sort them now self._storage_playground.devicetree.actions.prune() self._storage_playground.devicetree.actions.sort() NormalSpoke.on_back_clicked(self, button) def on_summary_button_clicked(self, _button): self.blivetgui.show_actions() def on_undo_action_button_clicked(self, _button): self.blivetgui.actions_undo() # This callback is for the button that just resets the UI to anaconda's # current understanding of the disk layout. def on_reset_button_clicked(self, *args): msg = _( "Continuing with this action will reset all your partitioning selections " "to their current on-disk state.") dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.NONE, message_format=msg) dlg.set_decorated(False) dlg.add_buttons( C_("GUI|Custom Partitioning|Reset Dialog", "_Reset selections"), 0, C_("GUI|Custom Partitioning|Reset Dialog", "_Preserve current selections"), 1) dlg.set_default_response(1) with self.main_window.enlightbox(dlg): rc = dlg.run() dlg.destroy() if rc == 0: self.refresh() self.blivetgui.reload() # XXX: Reset currently preserves actions set in previous runs # of the spoke, so we need to 're-add' these to the ui current_actions = self._storage_playground.devicetree.actions.find( ) if current_actions: self.blivetgui.set_actions(current_actions)
class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): """ .. inheritance-diagram:: DatetimeSpoke :parts: 3 """ builderObjects = [ "datetimeWindow", "days", "months", "years", "regions", "cities", "upImage", "upImage1", "upImage2", "downImage", "downImage1", "downImage2", "downImage3", "configImage", "citiesFilter", "daysFilter", "cityCompletion", "regionCompletion", ] mainWidgetName = "datetimeWindow" uiFile = "spokes/datetime_spoke.glade" helpFile = "DateTimeSpoke.xml" category = LocalizationCategory icon = "preferences-system-time-symbolic" title = CN_("GUI|Spoke", "_Time & Date") # Hack to get libtimezonemap loaded for GtkBuilder # see https://bugzilla.gnome.org/show_bug.cgi?id=712184 _hack = TimezoneMap.TimezoneMap() del (_hack) def __init__(self, *args): NormalSpoke.__init__(self, *args) # taking values from the kickstart file? self._kickstarted = flags.flags.automatedInstall self._update_datetime_timer = None self._start_updating_timer = None self._shown = False self._tz = None self._timezone_module = TIMEZONE.get_observer() self._timezone_module.connect() self._network_module = NETWORK.get_observer() self._network_module.connect() def initialize(self): NormalSpoke.initialize(self) self.initialize_start() self._daysStore = self.builder.get_object("days") self._monthsStore = self.builder.get_object("months") self._yearsStore = self.builder.get_object("years") self._regionsStore = self.builder.get_object("regions") self._citiesStore = self.builder.get_object("cities") self._tzmap = self.builder.get_object("tzmap") self._dateBox = self.builder.get_object("dateBox") # we need to know it the new value is the same as previous or not self._old_region = None self._old_city = None self._regionCombo = self.builder.get_object("regionCombobox") self._cityCombo = self.builder.get_object("cityCombobox") self._daysFilter = self.builder.get_object("daysFilter") self._daysFilter.set_visible_func(self.existing_date, None) self._citiesFilter = self.builder.get_object("citiesFilter") self._citiesFilter.set_visible_func(self.city_in_region, None) self._hoursLabel = self.builder.get_object("hoursLabel") self._minutesLabel = self.builder.get_object("minutesLabel") self._amPmUp = self.builder.get_object("amPmUpButton") self._amPmDown = self.builder.get_object("amPmDownButton") self._amPmLabel = self.builder.get_object("amPmLabel") self._radioButton24h = self.builder.get_object("timeFormatRB") self._amPmRevealer = self.builder.get_object("amPmRevealer") # Set the entry completions. # The text_column property needs to be set here. If we set # it in the glade file, the completion doesn't show text. region_completion = self.builder.get_object("regionCompletion") region_completion.set_text_column(0) city_completion = self.builder.get_object("cityCompletion") city_completion.set_text_column(0) # create widgets for displaying/configuring date day_box, self._dayCombo, day_label = _new_date_field_box( self._daysFilter) self._dayCombo.connect("changed", self.on_day_changed) month_box, self._monthCombo, month_label = _new_date_field_box( self._monthsStore) self._monthCombo.connect("changed", self.on_month_changed) year_box, self._yearCombo, year_label = _new_date_field_box( self._yearsStore) self._yearCombo.connect("changed", self.on_year_changed) # get the right order for date widgets and respective formats and put # widgets in place widgets, formats = resolve_date_format(year_box, month_box, day_box) for widget in widgets: self._dateBox.pack_start(widget, False, False, 0) self._day_format, suffix = formats[widgets.index(day_box)] day_label.set_text(suffix) self._month_format, suffix = formats[widgets.index(month_box)] month_label.set_text(suffix) self._year_format, suffix = formats[widgets.index(year_box)] year_label.set_text(suffix) self._ntpSwitch = self.builder.get_object("networkTimeSwitch") self._regions_zones = get_all_regions_and_timezones() # Set the initial sensitivity of the AM/PM toggle based on the time-type selected self._radioButton24h.emit("toggled") if not conf.system.can_set_system_clock: self._set_date_time_setting_sensitive(False) self._config_dialog = NTPconfigDialog(self.data, self._timezone_module) self._config_dialog.initialize() threadMgr.add( AnacondaThread(name=constants.THREAD_DATE_TIME, target=self._initialize)) def _initialize(self): # a bit hacky way, but should return the translated strings for i in range(1, 32): day = datetime.date(2000, 1, i).strftime(self._day_format) self.add_to_store_idx(self._daysStore, i, day) for i in range(1, 13): month = datetime.date(2000, i, 1).strftime(self._month_format) self.add_to_store_idx(self._monthsStore, i, month) for i in range(1990, 2051): year = datetime.date(i, 1, 1).strftime(self._year_format) self.add_to_store_idx(self._yearsStore, i, year) cities = set() xlated_regions = ((region, get_xlated_timezone(region)) for region in self._regions_zones.keys()) for region, xlated in sorted( xlated_regions, key=functools.cmp_to_key(_compare_regions)): self.add_to_store_xlated(self._regionsStore, region, xlated) for city in self._regions_zones[region]: cities.add((city, get_xlated_timezone(city))) for city, xlated in sorted(cities, key=functools.cmp_to_key(_compare_cities)): self.add_to_store_xlated(self._citiesStore, city, xlated) self._update_datetime_timer = None kickstart_timezone = self._timezone_module.proxy.Timezone if is_valid_timezone(kickstart_timezone): self._set_timezone(kickstart_timezone) elif not flags.flags.automatedInstall: log.warning( "%s is not a valid timezone, falling back to default (%s)", kickstart_timezone, DEFAULT_TZ) self._set_timezone(DEFAULT_TZ) self._timezone_module.proxy.SetTimezone(DEFAULT_TZ) time_init_thread = threadMgr.get(constants.THREAD_TIME_INIT) if time_init_thread is not None: hubQ.send_message(self.__class__.__name__, _("Restoring hardware time...")) threadMgr.wait(constants.THREAD_TIME_INIT) hubQ.send_ready(self.__class__.__name__, False) # report that we are done self.initialize_done() @property def status(self): kickstart_timezone = self._timezone_module.proxy.Timezone if kickstart_timezone: if is_valid_timezone(kickstart_timezone): return _("%s timezone") % get_xlated_timezone( kickstart_timezone) else: return _("Invalid timezone") else: location = self._tzmap.get_location() if location and location.get_property("zone"): return _("%s timezone") % get_xlated_timezone( location.get_property("zone")) else: return _("Nothing selected") def apply(self): self._shown = False # we could use self._tzmap.get_timezone() here, but it returns "" if # Etc/XXXXXX timezone is selected region = self._get_active_region() city = self._get_active_city() # nothing selected, just leave the spoke and # return to hub without changing anything if not region or not city: return old_tz = self._timezone_module.proxy.Timezone new_tz = region + "/" + city self._timezone_module.proxy.SetTimezone(new_tz) if old_tz != new_tz: # new values, not from kickstart # TODO: seen should be set from the module self._kickstarted = False self._timezone_module.proxy.SetNTPEnabled(self._ntpSwitch.get_active()) def execute(self): if self._update_datetime_timer is not None: self._update_datetime_timer.cancel() self._update_datetime_timer = None @property def ready(self): return not threadMgr.get("AnaDateTimeThread") @property def completed(self): if self._kickstarted and not self._timezone_module.proxy.Kickstarted: # taking values from kickstart, but not specified return False else: return is_valid_timezone(self._timezone_module.proxy.Timezone) @property def mandatory(self): return True def refresh(self): self._shown = True # update the displayed time self._update_datetime_timer = Timer() self._update_datetime_timer.timeout_sec(1, self._update_datetime) self._start_updating_timer = None kickstart_timezone = self._timezone_module.proxy.Timezone if is_valid_timezone(kickstart_timezone): self._tzmap.set_timezone(kickstart_timezone) time.tzset() self._update_datetime() has_active_network = self._network_module.proxy.Connected if not has_active_network: self._show_no_network_warning() else: self.clear_info() gtk_call_once(self._config_dialog.refresh_servers_state) if conf.system.can_set_time_synchronization: ntp_working = has_active_network and util.service_running( NTP_SERVICE) else: ntp_working = self._timezone_module.proxy.NTPEnabled self._ntpSwitch.set_active(ntp_working) @async_action_wait def _set_timezone(self, timezone): """ Sets timezone to the city/region comboboxes and the timezone map. :param timezone: timezone to set :type timezone: str :return: if successfully set or not :rtype: bool """ parts = timezone.split("/", 1) if len(parts) != 2: # invalid timezone cannot be set return False region, city = parts self._set_combo_selection(self._regionCombo, region) self._set_combo_selection(self._cityCombo, city) return True @async_action_nowait def add_to_store_xlated(self, store, item, xlated): store.append([item, xlated]) @async_action_nowait def add_to_store_idx(self, store, idx, item): store.append([idx, item]) def existing_date(self, days_model, days_iter, user_data=None): if not days_iter: return False day = days_model[days_iter][0] #days 1-28 are in every month every year if day < 29: return True months_model = self._monthCombo.get_model() months_iter = self._monthCombo.get_active_iter() if not months_iter: return True years_model = self._yearCombo.get_model() years_iter = self._yearCombo.get_active_iter() if not years_iter: return True try: datetime.date(years_model[years_iter][0], months_model[months_iter][0], day) return True except ValueError: return False def _get_active_city(self): cities_model = self._cityCombo.get_model() cities_iter = self._cityCombo.get_active_iter() if not cities_iter: return None return cities_model[cities_iter][0] def _get_active_region(self): regions_model = self._regionCombo.get_model() regions_iter = self._regionCombo.get_active_iter() if not regions_iter: return None return regions_model[regions_iter][0] def city_in_region(self, model, itr, user_data=None): if not itr: return False city = model[itr][0] region = self._get_active_region() if not region: return False return city in self._regions_zones[region] def _set_amPm_part_sensitive(self, sensitive): for widget in (self._amPmUp, self._amPmDown, self._amPmLabel): widget.set_sensitive(sensitive) def _to_amPm(self, hours): if hours >= 12: day_phase = _("PM") else: day_phase = _("AM") new_hours = ((hours - 1) % 12) + 1 return (new_hours, day_phase) def _to_24h(self, hours, day_phase): correction = 0 if day_phase == _("AM") and hours == 12: correction = -12 elif day_phase == _("PM") and hours != 12: correction = 12 return (hours + correction) % 24 def _update_datetime(self): now = datetime.datetime.now(self._tz) if self._radioButton24h.get_active(): self._hoursLabel.set_text("%0.2d" % now.hour) else: hours, amPm = self._to_amPm(now.hour) self._hoursLabel.set_text("%0.2d" % hours) self._amPmLabel.set_text(amPm) self._minutesLabel.set_text("%0.2d" % now.minute) self._set_combo_selection(self._dayCombo, now.day) self._set_combo_selection(self._monthCombo, now.month) self._set_combo_selection(self._yearCombo, now.year) #GLib's timer is driven by the return value of the function. #It runs the fuction periodically while the returned value #is True. return True def _save_system_time(self): """ Returning False from this method removes the timer that would otherwise call it again and again. """ self._start_updating_timer = None if not conf.system.can_set_system_clock: return False month = self._get_combo_selection(self._monthCombo)[0] if not month: return False year = self._get_combo_selection(self._yearCombo)[0] if not year: return False hours = int(self._hoursLabel.get_text()) if not self._radioButton24h.get_active(): hours = self._to_24h(hours, self._amPmLabel.get_text()) minutes = int(self._minutesLabel.get_text()) day = self._get_combo_selection(self._dayCombo)[0] #day may be None if there is no such in the selected year and month if day: isys.set_system_date_time(year, month, day, hours, minutes, tz=self._tz) #start the timer only when the spoke is shown if self._shown and not self._update_datetime_timer: self._update_datetime_timer = Timer() self._update_datetime_timer.timeout_sec(1, self._update_datetime) #run only once (after first 2 seconds of inactivity) return False def _stop_and_maybe_start_time_updating(self, interval=2): """ This method is called in every date/time-setting button's callback. It removes the timer for updating displayed date/time (do not want to change it while user does it manually) and allows us to set new system date/time only after $interval seconds long idle on time-setting buttons. This is done by the _start_updating_timer that is reset in this method. So when there is $interval seconds long idle on date/time-setting buttons, self._save_system_time method is invoked. Since it returns False, this timer is then removed and only reactivated in this method (thus in some date/time-setting button's callback). """ #do not start timers if the spoke is not shown if not self._shown: self._update_datetime() self._save_system_time() return #stop time updating if self._update_datetime_timer: self._update_datetime_timer.cancel() self._update_datetime_timer = None #stop previous $interval seconds timer (see below) if self._start_updating_timer: self._start_updating_timer.cancel() #let the user change date/time and after $interval seconds of inactivity #save it as the system time and start updating the displayed date/time self._start_updating_timer = Timer() self._start_updating_timer.timeout_sec(interval, self._save_system_time) def _set_combo_selection(self, combo, item): model = combo.get_model() if not model: return False itr = model.get_iter_first() while itr: if model[itr][0] == item: combo.set_active_iter(itr) return True itr = model.iter_next(itr) return False def _get_combo_selection(self, combo): """ Get the selected item of the combobox. :return: selected item or None """ model = combo.get_model() itr = combo.get_active_iter() if not itr or not model: return None, None return model[itr][0], model[itr][1] def _restore_old_city_region(self): """Restore stored "old" (or last valid) values.""" # check if there are old values to go back to if self._old_region and self._old_city: self._set_timezone(self._old_region + "/" + self._old_city) def on_up_hours_clicked(self, *args): self._stop_and_maybe_start_time_updating() hours = int(self._hoursLabel.get_text()) if self._radioButton24h.get_active(): new_hours = (hours + 1) % 24 else: amPm = self._amPmLabel.get_text() #let's not deal with magical AM/PM arithmetics new_hours = self._to_24h(hours, amPm) new_hours, new_amPm = self._to_amPm((new_hours + 1) % 24) self._amPmLabel.set_text(new_amPm) new_hours_str = "%0.2d" % new_hours self._hoursLabel.set_text(new_hours_str) def on_down_hours_clicked(self, *args): self._stop_and_maybe_start_time_updating() hours = int(self._hoursLabel.get_text()) if self._radioButton24h.get_active(): new_hours = (hours - 1) % 24 else: amPm = self._amPmLabel.get_text() #let's not deal with magical AM/PM arithmetics new_hours = self._to_24h(hours, amPm) new_hours, new_amPm = self._to_amPm((new_hours - 1) % 24) self._amPmLabel.set_text(new_amPm) new_hours_str = "%0.2d" % new_hours self._hoursLabel.set_text(new_hours_str) def on_up_minutes_clicked(self, *args): self._stop_and_maybe_start_time_updating() minutes = int(self._minutesLabel.get_text()) minutes_str = "%0.2d" % ((minutes + 1) % 60) self._minutesLabel.set_text(minutes_str) def on_down_minutes_clicked(self, *args): self._stop_and_maybe_start_time_updating() minutes = int(self._minutesLabel.get_text()) minutes_str = "%0.2d" % ((minutes - 1) % 60) self._minutesLabel.set_text(minutes_str) def on_updown_ampm_clicked(self, *args): self._stop_and_maybe_start_time_updating() if self._amPmLabel.get_text() == _("AM"): self._amPmLabel.set_text(_("PM")) else: self._amPmLabel.set_text(_("AM")) def on_region_changed(self, combo, *args): """ :see: on_city_changed """ region = self._get_active_region() if not region or region == self._old_region: # region entry being edited or old_value chosen, no action needed # @see: on_city_changed return self._citiesFilter.refilter() # Set the city to the first one available in this newly selected region. zone = self._regions_zones[region] firstCity = sorted(list(zone))[0] self._set_combo_selection(self._cityCombo, firstCity) self._old_region = region self._old_city = firstCity def on_city_changed(self, combo, *args): """ ComboBox emits ::changed signal not only when something is selected, but also when its entry's text is changed. We need to distinguish between those two cases ('London' typed in the entry => no action until ENTER is hit etc.; 'London' chosen in the expanded combobox => update timezone map and do all necessary actions). Fortunately when entry is being edited, self._get_active_city returns None. """ timezone = None region = self._get_active_region() city = self._get_active_city() if not region or not city or (region == self._old_region and city == self._old_city): # entry being edited or no change, no actions needed return if city and region: timezone = region + "/" + city else: # both city and region are needed to form a valid timezone return if region == "Etc": # Etc timezones cannot be displayed on the map, so let's reset the # location and manually set a highlight with no location pin. self._tzmap.clear_location() if city in ("GMT", "UTC"): offset = 0.0 # The tzdb data uses POSIX-style signs for the GMT zones, which is # the opposite of whatever everyone else expects. GMT+4 indicates a # zone four hours west of Greenwich; i.e., four hours before. Reverse # the sign to match the libtimezone map. else: # Take the part after "GMT" offset = -float(city[3:]) self._tzmap.set_selected_offset(offset) time.tzset() else: # we don't want the timezone-changed signal to be emitted self._tzmap.set_timezone(timezone) time.tzset() # update "old" values self._old_city = city def on_entry_left(self, entry, *args): # user clicked somewhere else or hit TAB => finished editing entry.emit("activate") def on_city_region_key_released(self, entry, event, *args): if event.type == Gdk.EventType.KEY_RELEASE and \ event.keyval == Gdk.KEY_Escape: # editing canceled self._restore_old_city_region() def on_completion_match_selected(self, combo, model, itr): item = None if model and itr: item = model[itr][0] if item: self._set_combo_selection(combo, item) def on_city_region_text_entry_activated(self, entry): combo = entry.get_parent() # It's gotta be up there somewhere, right? right??? while not isinstance(combo, Gtk.ComboBox): combo = combo.get_parent() model = combo.get_model() entry_text = entry.get_text().lower() for row in model: if entry_text == row[0].lower(): self._set_combo_selection(combo, row[0]) return # non-matching value entered, reset to old values self._restore_old_city_region() def on_month_changed(self, *args): self._stop_and_maybe_start_time_updating(interval=5) self._daysFilter.refilter() def on_day_changed(self, *args): self._stop_and_maybe_start_time_updating(interval=5) def on_year_changed(self, *args): self._stop_and_maybe_start_time_updating(interval=5) self._daysFilter.refilter() def on_location_changed(self, tz_map, location): if not location: return timezone = location.get_property('zone') # Updating the timezone will update the region/city combo boxes to match. # The on_city_changed handler will attempt to convert the timezone back # to a location and set it in the map, which we don't want, since we # already have a location. That's why we're here. with blockedHandler(self._cityCombo, self.on_city_changed): if self._set_timezone(timezone): # timezone successfully set self._tz = get_timezone(timezone) self._update_datetime() def on_timeformat_changed(self, button24h, *args): hours = int(self._hoursLabel.get_text()) amPm = self._amPmLabel.get_text() #connected to 24-hour radio button if button24h.get_active(): self._set_amPm_part_sensitive(False) new_hours = self._to_24h(hours, amPm) self._amPmRevealer.set_reveal_child(False) else: self._set_amPm_part_sensitive(True) new_hours, new_amPm = self._to_amPm(hours) self._amPmLabel.set_text(new_amPm) self._amPmRevealer.set_reveal_child(True) self._hoursLabel.set_text("%0.2d" % new_hours) def _set_date_time_setting_sensitive(self, sensitive): #contains all date/time setting widgets footer_alignment = self.builder.get_object("footerAlignment") footer_alignment.set_sensitive(sensitive) def _show_no_network_warning(self): self.set_warning(_("You need to set up networking first if you "\ "want to use NTP")) def _show_no_ntp_server_warning(self): self.set_warning(_("You have no working NTP server configured")) def on_ntp_switched(self, switch, *args): if switch.get_active(): #turned ON if not conf.system.can_set_time_synchronization: #cannot touch runtime system, not much to do here return if not self._network_module.proxy.Connected: self._show_no_network_warning() switch.set_active(False) return else: self.clear_info() working_server = self._config_dialog.working_server if working_server is None: self._show_no_ntp_server_warning() else: #we need a one-time sync here, because chronyd would not change #the time as drastically as we need ntp.one_time_sync_async(working_server) ret = util.start_service(NTP_SERVICE) self._set_date_time_setting_sensitive(False) #if starting chronyd failed and chronyd is not running, #set switch back to OFF if (ret != 0) and not util.service_running(NTP_SERVICE): switch.set_active(False) else: #turned OFF if not conf.system.can_set_time_synchronization: #cannot touch runtime system, nothing to do here return self._set_date_time_setting_sensitive(True) ret = util.stop_service(NTP_SERVICE) #if stopping chronyd failed and chronyd is running, #set switch back to ON if (ret != 0) and util.service_running(NTP_SERVICE): switch.set_active(True) self.clear_info() def on_ntp_config_clicked(self, *args): self._config_dialog.refresh() with self.main_window.enlightbox(self._config_dialog.window): response = self._config_dialog.run() if response == 1: pools, servers = self._config_dialog.pools_servers self._timezone_module.proxy.SetNTPServers( ntp.pools_servers_to_internal(pools, servers)) if self._config_dialog.working_server is None: self._show_no_ntp_server_warning() else: self.clear_info()
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: PasswordSpoke :parts: 3 """ builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" focusWidgetName = "password_entry" uiFile = "spokes/root_password.glade" helpFile = "PasswordSpoke.xml" category = UserSettingsCategory icon = "dialog-password-symbolic" title = CN_("GUI|Spoke", "_Root Account") @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(USERS): return False return FirstbootSpokeMixIn.should_run(environment, data) def __init__(self, *args): NormalSpoke.__init__(self, *args) GUISpokeInputCheckHandler.__init__(self) self._users_module = USERS.get_proxy() def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # get object references from the builders self._password_entry = self.builder.get_object("password_entry") self._password_confirmation_entry = self.builder.get_object("password_confirmation_entry") self._password_bar = self.builder.get_object("password_bar") self._password_label = self.builder.get_object("password_label") self._enable_root_radio = self.builder.get_object("enable_root_radio") self._disable_root_radio = self.builder.get_object("disable_root_radio") self._root_password_ssh_login_override = self.builder.get_object("root_password_ssh_login_override") self._revealer = self.builder.get_object("password_revealer") # Install the password checks: # - Has a password been specified? # - If a password has been specified and there is data in the confirm box, do they match? # - How strong is the password? # - Does the password contain non-ASCII characters? # Setup the password checker for password checking self._checker = input_checking.PasswordChecker( initial_password_content=self.password, initial_password_confirmation_content=self.password_confirmation, policy_name=PASSWORD_POLICY_ROOT ) # configure the checker for password checking self.checker.secret_type = constants.SecretType.PASSWORD # remove any placeholder texts if either password or confirmation field changes content from initial state self.checker.password.changed_from_initial_state.connect(self.remove_placeholder_texts) self.checker.password_confirmation.changed_from_initial_state.connect(self.remove_placeholder_texts) # connect UI updates to check results self.checker.checks_done.connect(self._checks_done) # check that the password is not empty self._empty_check = input_checking.PasswordEmptyCheck() # check that the content of the password field & the conformation field are the same self._confirm_check = input_checking.PasswordConfirmationCheck() # check password validity, quality and strength self._validity_check = input_checking.PasswordValidityCheck() # connect UI updates to validity check results self._validity_check.result.password_score_changed.connect(self.set_password_score) self._validity_check.result.status_text_changed.connect(self.set_password_status) # check if the password contains non-ascii characters self._ascii_check = input_checking.PasswordASCIICheck() # register the individual checks with the checker in proper order # 1) is the password non-empty ? # 2) are both entered passwords the same ? # 3) is the password valid according to the current password checking policy ? # 4) is the password free of non-ASCII characters ? self.checker.add_check(self._empty_check) self.checker.add_check(self._confirm_check) self.checker.add_check(self._validity_check) self.checker.add_check(self._ascii_check) # Set placeholders if the password has been set outside of the Anaconda # GUI we either don't really know anything about it if it's crypted # and still would not really want to expose it if its set in plaintext, # and thus can'treally show it in the UI in any meaningful way. if self._users_module.IsRootPasswordSet: password_set_message = _("Root password has been set.") self.password_entry.set_placeholder_text(password_set_message) self.password_confirmation_entry.set_placeholder_text(password_set_message) # Configure levels for the password bar self._password_bar.add_offset_value("low", 2) self._password_bar.add_offset_value("medium", 3) self._password_bar.add_offset_value("high", 4) # set visibility of the password entries # - without this the password visibility toggle icon will # not be shown set_password_visibility(self.password_entry, False) set_password_visibility(self.password_confirmation_entry, False) # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__) # report that we are done self.initialize_done() def refresh(self): # set the locked/unlocked state based on DBus data if self._users_module.IsRootAccountLocked: control = self._disable_root_radio else: control = self._enable_root_radio control.set_active(True) self.on_root_enabled_changed(control) self._root_password_ssh_login_override.set_active( self._users_module.RootPasswordSSHLoginAllowed ) if self.root_enabled: # rerun checks so that we have a correct status message, if any self.checker.run_checks() # focus on the password field if it is sensitive if self.password_entry.get_sensitive(): self.password_entry.grab_focus() @property def status(self): if self._users_module.IsRootAccountLocked: # reconfig mode currently allows re-enabling a locked root account if # user sets a new root password if is_reconfiguration_mode() and not self.root_enabled: return _("Disabled, set password to enable.") else: return _("Root account is disabled.") elif self._users_module.IsRootPasswordSet: return _("Root password is set") else: return _("Root password is not set") @property def mandatory(self): """Only mandatory if no admin user has been requested.""" return not self._users_module.CheckAdminUserExists() def apply(self): pw = self.password enabled = self.root_enabled self._users_module.SetRootAccountLocked(not enabled) if enabled: # the checkbox makes it possible to override the default Open SSH # policy of not allowing root to login with password ssh_login_override = self._root_password_ssh_login_override.get_active() self._users_module.SetRootPasswordSSHLoginAllowed(ssh_login_override) if not pw: self._users_module.ClearRootPassword() return # we have a password - set it to kickstart data self._users_module.SetCryptedRootPassword(crypt_password(pw)) # clear any placeholders self.remove_placeholder_texts() # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__) @property def completed(self): return self._users_module.IsRootPasswordSet @property def sensitive(self): # A password set in kickstart can be changed in the GUI # if the changesok password policy is set for the root password. kickstarted_password_can_be_changed = conf.ui.can_change_root or \ self._users_module.CanChangeRootPassword return not (self.completed and flags.automatedInstall and not kickstarted_password_can_be_changed) @property def root_enabled(self): return self._enable_root_radio.get_active() def _checks_done(self, error_message): """Update the warning with the input validation error from the first error message or clear warnings if all the checks were successful. Also appends the "press twice" suffix if compatible with current password policy and handles the press-done-twice logic. """ # check if an unwaivable check failed unwaivable_check_failed = not self._confirm_check.result.success # set appropriate status bar message if not error_message or not self.root_enabled: # all is fine, just clear the message self.clear_info() elif not self.password and not self.password_confirmation: # Clear any info message if both the password and password # confirmation fields are empty. # This shortcut is done to make it possible for the user to leave the spoke # without inputting any root password. Separate logic makes sure an # empty string is not set as the root password. self.clear_info() else: if self.checker.policy.is_strict or unwaivable_check_failed: # just forward the error message self.show_warning_message(error_message) else: # add suffix for the click twice logic self.show_warning_message("{} {}".format(error_message, _(constants.PASSWORD_DONE_TWICE))) # check if the spoke can be exited after the latest round of checks self._check_spoke_exit_conditions(unwaivable_check_failed) def _check_spoke_exit_conditions(self, unwaivable_check_failed): # Check if the user can escape from the root spoke or stay forever ! # reset any waiving in progress self.waive_clicks = 0 # Depending on the policy we allow users to waive the password strength # and non-ASCII checks. If the policy is set to strict, the password # needs to be strong, but can still contain non-ASCII characters. self.can_go_back = False self.needs_waiver = True # This shortcut is done to make it possible for the user to leave the spoke # without inputting any root password. Separate logic makes sure an # empty string is not set as the root password. if not self.password and not self.password_confirmation: self.can_go_back = True self.needs_waiver = False elif self.checker.success: # if all checks were successful we can always go back to the hub self.can_go_back = True self.needs_waiver = False elif unwaivable_check_failed: self.can_go_back = False else: if self.checker.policy.is_strict: if not self._validity_check.result.success: # failing validity check in strict # mode prevents us from going back self.can_go_back = False elif not self._ascii_check.result.success: # but the ASCII check can still be waived self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False else: if not self._validity_check.result.success: self.can_go_back = True self.needs_waiver = True elif not self._ascii_check.result.success: self.can_go_back = True self.needs_waiver = True else: self.can_go_back = True self.needs_waiver = False def on_password_changed(self, editable, data=None): """Tell checker that the content of the password field changed.""" self.checker.password.content = self.password def on_password_confirmation_changed(self, editable, data=None): """Tell checker that the content of the password confirmation field changed.""" self.checker.password_confirmation.content = self.password_confirmation def on_password_icon_clicked(self, entry, icon_pos, event): """Called by Gtk callback when the icon of a password entry is clicked.""" set_password_visibility(entry, not entry.get_visibility()) def on_back_clicked(self, button): # disable root if no password is entered if not self.password and not self.password_confirmation and \ not self._users_module.IsRootPasswordSet: control = self._disable_root_radio control.set_active(True) self.on_root_enabled_changed(control) # the GUI spoke input check handler handles the rest of the spoke exit logic for us if self.try_to_go_back() or not self.root_enabled: NormalSpoke.on_back_clicked(self, button) else: log.info("Return to hub prevented by password checking rules.") def on_root_enabled_changed(self, control): """Click event handler for root account enable and disable radio buttons.""" unlocked = (control == self._enable_root_radio) self._revealer.set_reveal_child(unlocked) if unlocked: self.password_entry.grab_focus()
class LangsupportSpoke(LangLocaleHandler, NormalSpoke): """ .. inheritance-diagram:: LangsupportSpoke :parts: 3 """ builderObjects = [ "languageStore", "languageStoreFilter", "localeStore", "langsupportWindow" ] mainWidgetName = "langsupportWindow" focusWidgetName = "languageEntry" uiFile = "spokes/language_support.glade" helpFile = "LangSupportSpoke.xml" category = LocalizationCategory icon = "accessories-character-map-symbolic" title = CN_("GUI|Spoke", "_LANGUAGE SUPPORT") def __init__(self, *args, **kwargs): NormalSpoke.__init__(self, *args, **kwargs) LangLocaleHandler.__init__(self) self._selected_locales = set() def initialize(self): self.initialize_start() self._languageStore = self.builder.get_object("languageStore") self._languageEntry = self.builder.get_object("languageEntry") self._languageStoreFilter = self.builder.get_object( "languageStoreFilter") self._langView = self.builder.get_object("languageView") self._langSelectedRenderer = self.builder.get_object( "langSelectedRenderer") self._langSelectedColumn = self.builder.get_object( "langSelectedColumn") self._langSelection = self.builder.get_object("languageViewSelection") self._localeStore = self.builder.get_object("localeStore") self._localeView = self.builder.get_object("localeView") LangLocaleHandler.initialize(self) # mark selected locales and languages with selected locales bold localeNativeColumn = self.builder.get_object("localeNativeName") localeNativeNameRenderer = self.builder.get_object( "localeNativeNameRenderer") override_cell_property(localeNativeColumn, localeNativeNameRenderer, "weight", self._mark_selected_locale_bold) languageNameColumn = self.builder.get_object("nameColumn") nativeNameRenderer = self.builder.get_object("nativeNameRenderer") englishNameRenderer = self.builder.get_object("englishNameRenderer") override_cell_property(languageNameColumn, nativeNameRenderer, "weight", self._mark_selected_language_bold) override_cell_property(languageNameColumn, englishNameRenderer, "weight", self._mark_selected_language_bold) # If a language has selected locales, highlight every column so that # the row appears highlighted for col in self._langView.get_columns(): for rend in col.get_cells(): override_cell_property(col, rend, "cell-background-rgba", self._highlight_selected_language) # and also set an icon so that we don't depend on a color to convey information highlightedColumn = self.builder.get_object("highlighted") highlightedRenderer = self.builder.get_object("highlightedRenderer") override_cell_property(highlightedColumn, highlightedRenderer, "icon-name", self._render_lang_highlighted) # report that we are done self.initialize_done() def apply(self): # store only additional langsupport locales self.data.lang.addsupport = sorted(self._selected_locales - set([self.data.lang.lang])) def refresh(self): self._languageEntry.set_text("") self._selected_locales = set(self._installed_langsupports) # select the first locale from the "to be installed" langsupports self._select_locale(self._installed_langsupports[0]) @property def _installed_langsupports(self): return [self.data.lang.lang] + sorted(self.data.lang.addsupport) @property def showable(self): # don't show the language support spoke on live media and in single language mode return not flags.livecdInstall and not flags.singlelang @property def status(self): return ", ".join( localization.get_native_name(locale) for locale in self._installed_langsupports) @property def mandatory(self): return False @property def completed(self): return True def _filter_languages(self, langs): return self.instclass.filterSupportedLangs(self.data, langs) def _add_language(self, store, native, english, lang): native_span = '<span lang="%s">%s</span>' % \ (escape_markup(lang), escape_markup(native)) store.append([native_span, english, lang]) def _filter_locales(self, lang, locales): return self.instclass.filterSupportedLocales(self.data, lang, locales) def _add_locale(self, store, native, locale): native_span = '<span lang="%s">%s</span>' % \ (escape_markup(re.sub(r'\..*', '', locale)), escape_markup(native)) # native, locale, selected, additional store.append([ native_span, locale, locale in self._selected_locales, locale != self.data.lang.lang ]) def _mark_selected_locale_bold(self, column, renderer, model, itr, user_data=None): if model[itr][2]: return Pango.Weight.BOLD.real else: return Pango.Weight.NORMAL.real def _is_lang_selected(self, lang): lang_locales = set(localization.get_language_locales(lang)) return not lang_locales.isdisjoint(self._selected_locales) def _mark_selected_language_bold(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return Pango.Weight.BOLD.real else: return Pango.Weight.NORMAL.real def _highlight_selected_language(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return _HIGHLIGHT_COLOR else: return None def _render_lang_highlighted(self, column, renderer, model, itr, user_data=None): if self._is_lang_selected(model[itr][2]): return "emblem-ok-symbolic" else: return None # Signal handlers. def on_locale_toggled(self, renderer, path): itr = self._localeStore.get_iter(path) row = self._localeStore[itr] row[2] = not row[2] if row[2]: self._selected_locales.add(row[1]) else: self._selected_locales.remove(row[1])