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 = "BlivetGuiSpoke.xml" ### 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 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 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") self.blivetgui = osinstall.BlivetGUIAnaconda(self.client, self, box) 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. """ 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 = [] # 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.data.bootloader.bootDrive = "" 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) ### 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 UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): builderObjects = ["userCreationWindow"] mainWidgetName = "userCreationWindow" focusWidgetName = "t_fullname" 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 == ANACONDA_ENVIRON: return True elif environment == 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 == 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._oldweak = None def initialize(self): NormalSpoke.initialize(self) if self.data.user.userList: self._user = self.data.user.userList[0] else: self._user = self.data.UserData() self._wheel = self.data.GroupData(name="wheel") self._groupDict = {"wheel": self._wheel} # placeholders for the text boxes self.fullname = self.builder.get_object("t_fullname") self.username = self.builder.get_object("t_username") self.pw = self.builder.get_object("t_password") self.confirm = self.builder.get_object("t_verifypassword") self.admin = self.builder.get_object("c_admin") self.usepassword = self.builder.get_object("c_usepassword") self.b_advanced = self.builder.get_object("b_advanced") # Counters for checks that ask the user to click Done to confirm self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self.guesser = {self.username: True} # Updated during the password changed event and used by the password # field validity checker self._pw_error_message = None self._pw_score = 0 self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("user") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() # indicate when the password was set by kickstart self._user.password_kickstarted = self.data.user.seen if self._user.password_kickstarted: self.usepassword.set_active(self._user.password != "") if not self._user.isCrypted: self.pw.set_text(self._user.password) self.confirm.set_text(self._user.password) else: self.usepassword.set_active(True) self.pw.set_placeholder_text( _("The password was set by kickstart.")) self.confirm.set_placeholder_text( _("The password was set by kickstart.")) elif not self.policy.emptyok: # Policy is that a non-empty password is required self.usepassword.set_active(True) if not self.policy.emptyok: # User isn't allowed to change whether password is required or not self.usepassword.set_sensitive(False) # Password checks, in order of importance: # - if a password is required, is one specified? # - if a password is specified and there is data in the confirm box, do they match? # - if a password is specified and the confirm box is empty or match, how strong is it? # - if a strong password is specified, does it contain non-ASCII data? # - if a password is required, is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # The password confirmation needs to be checked whenever either of the password # fields change. Separate checks are created on each field so that edits on # either will trigger a check and so that the last edited field will get the focus # when Done is clicked. Whichever check is run needs to run the other check in # order to reset the status. The check_data field is used as a flag to prevent # infinite recursion. self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) self._password_check = self.add_check(self.pw, self._checkPasswordConfirm) # Keep a reference to these checks, since they have to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) self.add_check(self.confirm, self._checkPasswordEmpty) self.add_check(self.username, self._checkUsername) self.add_re_check(self.fullname, GECOS_VALID, _("Full name cannot contain colon characters")) self._advanced = AdvancedUserDialog(self._user, self._groupDict, self.data) self._advanced.initialize() def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.username.set_text(self._user.name) self.fullname.set_text(self._user.gecos) self.admin.set_active(self._wheel.name in self._user.groups) self.pw.emit("changed") self.confirm.emit("changed") if self.username.get_text() and self.usepassword.get_active() and \ self._user.password == "": self.pw.grab_focus() elif self.fullname.get_text(): self.username.grab_focus() else: self.fullname.grab_focus() self.b_advanced.set_sensitive(bool(self._user.name)) @property def status(self): if len(self.data.user.userList) == 0: return _("No user will be created") elif self._wheel.name 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.data.rootpw.password and not self.data.rootpw.lock 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.usepassword.get_active(): if self.pw.get_text(): self._user.password_kickstarted = False self._user.password = cryptPassword(self.pw.get_text()) self._user.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") # reset the password when the user unselects it else: self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") self._user.password = "" self._user.isCrypted = False self._user.password_kickstarted = False self._user.name = self.username.get_text() self._user.gecos = self.fullname.get_text() # Remove any groups that were created in a previous visit to this spoke self.data.group.groupList = [g for g in self.data.group.groupList \ if not hasattr(g, 'anaconda_group')] # the user will be created only if the username is set if self._user.name: if self.admin.get_active() and \ self._wheel.name not in self._user.groups: self._user.groups.append(self._wheel.name) elif not self.admin.get_active() and \ self._wheel.name in self._user.groups: self._user.groups.remove(self._wheel.name) anaconda_groups = [ self._groupDict[g] for g in self._user.groups if g != self._wheel.name ] self.data.group.groupList += anaconda_groups # Flag the groups as being created in this spoke for g in anaconda_groups: g.anaconda_group = True if self._user not in self.data.user.userList: self.data.user.userList.append(self._user) elif self._user in self.data.user.userList: self.data.user.userList.remove(self._user) @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.policy.changesok) @property def completed(self): return len(self.data.user.userList) > 0 def _updatePwQuality(self): """This method updates the password indicators according to the password entered by the user. """ pwtext = self.pw.get_text() username = self.username.get_text() # Reset the counters used for the "press Done twice" logic self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self._pw_score, status_text, _pw_quality, self._pw_error_message = validatePassword( pwtext, username, minlen=self.policy.minlen, empty_ok=self.policy.emptyok) self.pw_bar.set_value(self._pw_score) self.pw_label.set_text(status_text) def usepassword_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.""" self.pw.set_sensitive(self.usepassword.get_active()) self.confirm.set_sensitive(self.usepassword.get_active()) # Re-check the password self.pw.emit("changed") self.confirm.emit("changed") def password_changed(self, editable=None, data=None): """Update the password strength level bar""" self._updatePwQuality() def username_changed(self, editable=None, data=None): """Called by Gtk callback when the username or hostname entry changes. It disables the guess algorithm if the user added his own text there and reenable it when the user deletes the whole text.""" if editable.get_text() == "": self.guesser[editable] = True self.b_advanced.set_sensitive(False) else: self.guesser[editable] = False self.b_advanced.set_sensitive(True) # Re-run the password checks against the new username self.pw.emit("changed") self.confirm.emit("changed") def full_name_changed(self, editable=None, data=None): """Called by Gtk callback when the full name field changes. It guesses the username and hostname, strips diacritics and make those lowercase. """ # after the text is updated in guesser, the guess has to be reenabled if self.guesser[self.username]: fullname = self.fullname.get_text() username = guess_username(fullname) self.username.set_text(username) self.guesser[self.username] = True def _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all. This check is used for both the password and the confirmation. """ # If the password was set by kickstart, skip the strength check if self._user.password_kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK # Skip the check if no password is required if (not self.usepassword.get_active() ) or self._user.password_kickstarted: return InputCheck.CHECK_OK elif not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """If the user has entered confirmation data, check whether it matches the password.""" # Skip the check if no password is required if (not self.usepassword.get_active() ) or self._user.password_kickstarted: result = InputCheck.CHECK_OK elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK # If the check succeeded, reset the status of the other check object # Disable the current check to prevent a cycle inputcheck.enabled = False if result == InputCheck.CHECK_OK: if inputcheck == self._confirm_check: self._password_check.update_check_status() else: self._confirm_check.update_check_status() inputcheck.enabled = True return result def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. The password strength has already been checked in _updatePwQuality, called previously in the signal chain. This method converts the data set from there into an error message. The password strength check can be waived by pressing "Done" twice. This is controlled through the self._waiveStrengthClicks counter. The counter is set in on_back_clicked, which also re-runs this check manually. """ # Skip the check if no password is required if (not self.usepassword.get_active()) or \ ((not self.pw.get_text()) and (self._user.password_kickstarted)): return InputCheck.CHECK_OK # Check for validity errors # pw score == 0 & errors from libpwquality # - ignore if the strict flag in the password policy == False if not self._pw_score and self._pw_error_message and self.policy.strict: return self._pw_error_message # use strength from policy, not bars pw = self.pw.get_text() username = self.username.get_text() _pw_score, _status_text, pw_quality, _error_message = validatePassword( pw, username, minlen=self.policy.minlen, empty_ok=self.policy.emptyok) if pw_quality < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK elif self._waiveStrengthClicks == 1: if self._pw_error_message: return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR ) % self._pw_error_message else: return _(PASSWORD_WEAK_CONFIRM) else: # non-strict allows done to be clicked twice if self.policy.strict: done_msg = "" else: done_msg = _(PASSWORD_DONE_TWICE) if self._pw_error_message: return _(PASSWORD_WEAK_WITH_ERROR ) % self._pw_error_message + " " + done_msg else: return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK def _checkPasswordASCII(self, inputcheck): """Set an error message if the password contains non-ASCII characters. Like the password strength check, this check can be bypassed by pressing Done twice. """ # If Done has been clicked, waive the check if self._waiveASCIIClicks > 0: return InputCheck.CHECK_OK password = self.get_input(inputcheck.input_obj) if password and any(char not in PW_ASCII_CHARS for char in password): return _(PASSWORD_ASCII) return InputCheck.CHECK_OK def _checkUsername(self, inputcheck): name = self.get_input(inputcheck.input_obj) # Allow empty usernames so the spoke can be exited without creating a user if name == "": return InputCheck.CHECK_OK valid, msg = check_username(name) if valid: return InputCheck.CHECK_OK else: return msg or _("Invalid user name") def on_advanced_clicked(self, _button, data=None): """Handler for the Advanced.. button. It starts the Advanced dialog for setting homedit, uid, gid and groups. """ self._user.name = self.username.get_text() if self.admin.get_active() and \ self._wheel.name not in self._user.groups: self._user.groups.append(self._wheel.name) elif not self.admin.get_active() and \ self._wheel.name in self._user.groups: self._user.groups.remove(self._wheel.name) self._advanced.refresh() with self.main_window.enlightbox(self._advanced.window): self._advanced.run() self.admin.set_active(self._wheel.name in self._user.groups) def on_back_clicked(self, button): # If the failed check is for password strength or non-ASCII # characters, add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: self._waiveASCIIClicks += 1 self._pwASCIICheck.update_check_status() # If there is no user set, skip the checks if not self.username.get_text(): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
class StorageSpoke(NormalSpoke, StorageChecker): 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") def __init__(self, *args, **kwargs): StorageChecker.__init__(self) NormalSpoke.__init__(self, *args, **kwargs) self.applyOnSkip = True self._ready = False self.autoPartType = None self.encrypted = False self.passphrase = "" self.selected_disks = self.data.ignoredisk.onlyuse[:] # 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.disks = [] if not flags.automatedInstall: # default to using autopart for interactive installs self.data.autopart.autopart = True self.autopart = self.data.autopart.autopart self.autoPartType = None self.clearPartType = CLEARPART_TYPE_NONE self._previous_autopart = False self._last_clicked_overview = None self._cur_clicked_overview = None self._grabObjects() def _grabObjects(self): self._customPart = self.builder.get_object("customRadioButton") self._encrypted = self.builder.get_object("encryptionCheckbox") self._reclaim = self.builder.get_object("reclaimCheckbox") def _applyDiskSelection(self, use_names): onlyuse = use_names[:] for disk in (d for d in self.storage.disks if d.name in onlyuse): onlyuse.extend(d.name for d in disk.ancestors if d.name not in onlyuse) self.data.ignoredisk.onlyuse = onlyuse self.data.clearpart.drives = use_names[:] def apply(self): self._applyDiskSelection(self.selected_disks) self.data.autopart.autopart = self.autopart self.data.autopart.type = self.autoPartType self.data.autopart.encrypted = self.encrypted self.data.autopart.passphrase = self.passphrase self.clearPartType = CLEARPART_TYPE_NONE if self.data.bootloader.bootDrive and \ self.data.bootloader.bootDrive not in self.selected_disks: self.data.bootloader.bootDrive = "" self.storage.bootloader.reset() self.data.clearpart.initAll = True self.data.clearpart.type = self.clearPartType self.storage.config.update(self.data) self.storage.autoPartType = self.data.autopart.type self.storage.encryptedAutoPart = self.data.autopart.encrypted self.storage.encryptionPassphrase = self.data.autopart.passphrase # If autopart is selected we want to remove whatever has been # created/scheduled to make room for autopart. # If custom is selected, we want to leave alone any storage layout the # user may have set up before now. self.storage.config.clearNonExistent = self.data.autopart.autopart # refresh the autopart swap size suggestion with currently selected disks for request in self.storage.autoPartitionRequests: if request.fstype == "swap": disk_space = getAvailableDiskSpace(self.storage) request.size = swap_lib.swapSuggestion(disk_space=disk_space) break def execute(self): # Spawn storage execution as a separate thread so there's no big delay # going back from this spoke to the hub while StorageChecker.run runs. # Yes, this means there's a thread spawning another thread. Sorry. threadMgr.add( AnacondaThread(name=constants.THREAD_EXECUTE_STORAGE, target=self._doExecute)) def _doExecute(self): self._ready = False hubQ.send_not_ready(self.__class__.__name__) hubQ.send_message(self.__class__.__name__, _("Saving storage configuration...")) try: doKickstartStorage(self.storage, self.data, self.instclass) except (StorageError, KickstartValueError) as e: log.error("storage configuration failed: %s", e) StorageChecker.errors = str(e).split("\n") hubQ.send_message(self.__class__.__name__, _("Failed to save storage configuration...")) self.data.bootloader.bootDrive = "" self.data.ignoredisk.drives = [] self.data.ignoredisk.onlyuse = [] self.storage.config.update(self.data) self.storage.reset() self.disks = getDisks(self.storage.devicetree) # now set ksdata back to the user's specified config self._applyDiskSelection(self.selected_disks) except BootLoaderError as e: log.error("BootLoader setup failed: %s", e) StorageChecker.errors = str(e).split("\n") hubQ.send_message(self.__class__.__name__, _("Failed to save storage configuration...")) self.data.bootloader.bootDrive = "" else: if self.autopart: # this was already run as part of doAutoPartition. dumb. StorageChecker.errors = [] StorageChecker.warnings = [] self.run() finally: self._ready = True hubQ.send_ready(self.__class__.__name__, True) @property def completed(self): retval = (threadMgr.get(constants.THREAD_EXECUTE_STORAGE) is None and threadMgr.get(constants.THREAD_CHECK_STORAGE) is None and self.storage.rootDevice is not None and not self.errors) return retval @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready @property def showable(self): return not flags.dirInstall @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") if flags.automatedInstall and not self.storage.rootDevice: return msg elif self.data.ignoredisk.onlyuse: msg = P_(("%d disk selected"), ("%d disks selected"), len(self.data.ignoredisk.onlyuse)) % len( self.data.ignoredisk.onlyuse) if self.errors: msg = _("Error checking storage configuration") elif self.warnings: msg = _("Warning checking storage configuration") elif self.data.autopart.autopart: msg = _("Automatic partitioning selected") else: msg = _("Custom partitioning selected") return msg @property def localOverviews(self): return self.local_disks_box.get_children() @property def advancedOverviews(self): return filter( lambda child: isinstance(child, AnacondaWidgets.DiskOverview), self.specialized_disks_box.get_children()) 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 not event.type 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.localOverviews advanced_overviews = self.advancedOverviews # 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.disks = getDisks(self.storage.devicetree) # synchronize our local data store with the global ksdata disk_names = [d.name for d in self.disks] # don't put disks with hidden formats in selected_disks self.selected_disks = [ d for d in self.data.ignoredisk.onlyuse if d in disk_names ] self.autopart = self.data.autopart.autopart self.autoPartType = self.data.autopart.type if self.autoPartType is None: self.autoPartType = AUTOPART_TYPE_LVM self.encrypted = self.data.autopart.encrypted self.passphrase = self.data.autopart.passphrase self._previous_autopart = self.autopart # First, remove all non-button children. for child in self.localOverviews + self.advancedOverviews: 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. for disk in filter(isLocalDisk, self.disks): self._add_disk_overview(disk, self.local_disks_box) # 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 name in self.data.ignoredisk.onlyuse: if name not in disk_names: continue obj = self.storage.devicetree.getDeviceByName(name, hidden=True) if isLocalDisk(obj): continue self._add_disk_overview(obj, self.specialized_disks_box) # update the selections in the ui for overview in self.localOverviews + self.advancedOverviews: name = overview.get_property("name") overview.set_chosen(name in self.selected_disks) self._update_summary() if self.errors: self.set_warning( _("Error checking storage configuration. Click for details.")) elif self.warnings: self.set_warning( _("Warning checking storage configuration. Click for details." )) def initialize(self): NormalSpoke.initialize(self) self.local_disks_box = self.builder.get_object("local_disks_box") self.specialized_disks_box = self.builder.get_object( "specialized_disks_box") threadMgr.add( AnacondaThread(name=constants.THREAD_STORAGE_WATCHER, target=self._initialize)) def _add_disk_overview(self, disk, box): if disk.removable: kind = "drive-removable-media" else: kind = "drive-harddisk" if disk.serial: popup_info = "%s" % disk.serial else: popup_info = None # We don't want to display the whole huge WWID for a multipath device. # That makes the DO way too wide. if isinstance(disk, MultipathDevice): desc = disk.wwid.split(":") description = ":".join(desc[0:3]) + "..." + ":".join(desc[-4:]) else: description = disk.description free = self.storage.getFreeSpace(disks=[disk])[disk.name][0] overview = AnacondaWidgets.DiskOverview(description, kind, str(disk.size), _("%s free") % free, disk.name, popup=popup_info) box.pack_start(overview, False, False, 0) # FIXME: this will need to get smarter # # maybe a little function that resolves each item in onlyuse using # udev_resolve_devspec and compares that to the DiskDevice? overview.set_chosen(disk.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): hubQ.send_message(self.__class__.__name__, _("Probing storage...")) threadMgr.wait(constants.THREAD_STORAGE) threadMgr.wait(constants.THREAD_CUSTOM_STORAGE_INIT) self.disks = getDisks(self.storage.devicetree) # if there's only one disk, select it by default if len(self.disks) == 1 and not self.selected_disks: self._applyDiskSelection([self.disks[0].name]) self._ready = True hubQ.send_ready(self.__class__.__name__, False) def _update_summary(self): """ Update the summary based on the UI. """ count = 0 capacity = Size(bytes=0) free = Size(bytes=0) # pass in our disk list so hidden disks' free space is available free_space = self.storage.getFreeSpace(disks=self.disks) selected = [d for d in self.disks if d.name in self.selected_disks] for disk in selected: capacity += disk.size free += free_space[disk.name][0] count += 1 anySelected = count > 0 summary = (P_( "%(count)d disk selected; %(capacity)s capacity; %(free)s free", "%(count)d disks selected; %(capacity)s capacity; %(free)s free", count) % { "count": count, "capacity": capacity, "free": free }) summary_label = self.builder.get_object("summary_label") summary_label.set_text(summary) summary_label.set_sensitive(anySelected) summary_button = self.builder.get_object("summary_button") summary_button.set_visible(anySelected) self.builder.get_object("local_untouched_label").set_visible( anySelected) self.builder.get_object("special_untouched_label").set_visible( anySelected) self.builder.get_object("other_options_label").set_sensitive( anySelected) self.builder.get_object("other_options_grid").set_sensitive( anySelected) if len(self.disks) == 0: self.set_warning( _("No disks detected. Please shut down the computer, connect at least one disk, and restart to complete installation." )) elif not anySelected: self.set_warning( _("No disks selected; please select at least one disk to install to." )) else: self.clear_info() def _update_disk_list(self): """ Update self.selected_disks based on the UI. """ for overview in self.localOverviews + self.advancedOverviews: 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 # pass in our disk list so hidden disks' free space is available free_space = self.storage.getFreeSpace(disks=self.disks) dialog = SelectedDisksDialog(self.data, ) dialog.refresh( [d for d in self.disks if d.name in self.selected_disks], free_space) self.run_lightbox_dialog(dialog) # update selected disks since some may have been removed self.selected_disks = [d.name for d in dialog.disks] # update the UI to reflect changes to self.selected_disks for overview in self.localOverviews: name = overview.get_property("name") overview.set_chosen(name in self.selected_disks) self._update_summary() self.data.bootloader.seen = True if self.data.bootloader.location == "none": self.set_warning( _("You have chosen to skip bootloader installation. Your system may not be bootable." )) self.window.show_all() else: self.clear_info() def run_lightbox_dialog(self, dialog): with enlightbox(self.window, dialog.window): rc = dialog.run() return rc def _check_encrypted(self): # even if they're not doing autopart, setting autopart.encrypted # establishes a default of encrypting new devices if not self.encrypted: return True dialog = PassphraseDialog(self.data) rc = self.run_lightbox_dialog(dialog) if rc != 1: return False self.passphrase = dialog.passphrase return True def on_back_clicked(self, button): # We can't exit early if it looks like nothing has changed because the # user might want to change settings presented in the dialogs shown from # within this method. # Remove all non-existing devices if autopart was active when we last # refreshed. if self._previous_autopart: self._previous_autopart = False for partition in self.storage.partitions[:]: # check if it's been removed in a previous iteration if not partition.exists and \ partition in self.storage.partitions: self.storage.recursiveRemove(partition) # hide/unhide disks as requested for disk in self.disks: if disk.name not in self.selected_disks and \ disk in self.storage.devices: self.storage.devicetree.hide(disk) elif disk.name in self.selected_disks and \ disk not in self.storage.devices: self.storage.devicetree.unhide(disk) # show the installation options dialog disks = [d for d in self.disks if d.name in self.selected_disks] disks_size = sum((d.size for d in disks), Size(bytes=0)) # No disks selected? The user wants to back out of the storage spoke. if not disks: NormalSpoke.on_back_clicked(self, button) return # Figure out if the existing disk labels will work on this platform # you need to have at least one of the platform's labels in order for # any of the free space to be useful. disk_labels = set(disk.format.labelType for disk in disks if hasattr(disk.format, "labelType")) platform_labels = set(platform.diskLabelTypes) if disk_labels and platform_labels.isdisjoint(disk_labels): disk_free = 0 fs_free = 0 log.debug("Need disklabel: %s have: %s", ", ".join(platform_labels), ", ".join(disk_labels)) else: free_space = self.storage.getFreeSpace( disks=disks, clearPartType=CLEARPART_TYPE_NONE) disk_free = sum(f[0] for f in free_space.itervalues()) fs_free = sum(f[1] for f in free_space.itervalues()) required_space = self.payload.spaceRequired auto_swap = sum((r.size for r in self.storage.autoPartitionRequests if r.fstype == "swap"), Size(bytes=0)) log.debug("disk free: %s fs free: %s sw needs: %s auto swap: %s", disk_free, fs_free, required_space, auto_swap) if disk_free >= required_space + auto_swap: dialog = None elif disks_size >= required_space: if self._customPart.get_active() or self._reclaim.get_active(): dialog = None else: dialog = NeedSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, auto_swap, disk_free, fs_free) rc = self.run_lightbox_dialog(dialog) else: dialog = NoSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, auto_swap, disk_free, fs_free) rc = self.run_lightbox_dialog(dialog) if not dialog: # Plenty of room - there's no need to pop up a dialog, so just send # the user to wherever they asked to go. That's either the custom # spoke or the hub. # - OR - # Not enough room, but the user checked the reclaim button. # But first, we need to ask about an encryption passphrase if that # checkbox was active. self.encrypted = self._encrypted.get_active() if not self._check_encrypted(): return # Oh and then we might also want to go to the reclaim dialog. if self._reclaim.get_active(): self.apply() if not self._show_resize_dialog(disks): # User pressed cancel on the reclaim dialog, so don't leave # the storage spoke. return if self._customPart.get_active(): self.autopart = False self.skipTo = "CustomPartitioningSpoke" else: self.autopart = True elif 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. return elif rc == RESPONSE_MODIFY_SW: # The "Fedora software selection" link was clicked on one of the # dialogs. Send the user to the software spoke. self.skipTo = "SoftwareSelectionSpoke" elif rc == RESPONSE_RECLAIM: # Not enough space, but the user can make enough if they do some # work and free up space. self.encrypted = self._encrypted.get_active() if not self._check_encrypted(): return self.apply() if not self._show_resize_dialog(disks): # User pressed cancel on the reclaim dialog, so don't leave # the storage spoke. return # And then go to the custom partitioning spoke if they chose to # do so. if self._customPart.get_active(): self.autopart = False self.skipTo = "CustomPartitioningSpoke" else: self.autopart = True elif 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") else: # I don't know how we'd get here, but might as well have a # catch-all. Just stay on this spoke. return self.applyOnSkip = True NormalSpoke.on_back_clicked(self, button) def _show_resize_dialog(self, disks): resizeDialog = ResizeDialog(self.data, self.storage, self.payload) resizeDialog.refresh(disks) rc = self.run_lightbox_dialog(resizeDialog) return rc def on_custom_toggled(self, button): # The custom button won't be active until after this handler is run, # so we have to negate everything here. self._reclaim.set_sensitive(not button.get_active()) if self._reclaim.get_sensitive(): self._reclaim.set_has_tooltip(False) else: self._reclaim.set_tooltip_text( _("You'll be able to make space available during custom partitioning." )) 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. self._applyDiskSelection(self.selected_disks) self.skipTo = "FilterSpoke" NormalSpoke.on_back_clicked(self, button) def on_info_bar_clicked(self, *args): if 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 enlightbox(self.window, dialog.window): errors = "\n".join(self.errors) dialog.refresh(errors) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. 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=[_("_OK")], label=label) with enlightbox(self.window, 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.localOverviews elif box is self.specialized_disks_box: overviews = self.advancedOverviews else: # no other box contains disk overviews return for overview in overviews: overview.set_chosen(True) self._update_disk_list()
class StorageSpoke(NormalSpoke, StorageChecker): """ .. inheritance-diagram:: StorageSpoke :parts: 3 """ builderObjects = ["storageWindow", "addSpecializedImage"] mainWidgetName = "storageWindow" uiFile = "spokes/storage.glade" helpFile = "StorageSpoke.xml" category = SystemCategory # other candidates: computer-symbolic, folder-symbolic icon = "drive-harddisk-symbolic" title = CN_("GUI|Spoke", "INSTALLATION _DESTINATION") def __init__(self, *args, **kwargs): StorageChecker.__init__(self, min_ram=isys.MIN_GUI_RAM) NormalSpoke.__init__(self, *args, **kwargs) self.applyOnSkip = True self._ready = False self.autoPartType = None self.encrypted = False self.passphrase = "" self.selected_disks = self.data.ignoredisk.onlyuse[:] self._last_selected_disks = None self._back_clicked = False self.autopart_missing_passphrase = False self.disks_errors = [] # 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.disks = [] if not flags.automatedInstall: # default to using autopart for interactive installs self.data.autopart.autopart = True self.autopart = self.data.autopart.autopart self.autoPartType = None self.clearPartType = CLEARPART_TYPE_NONE if self.data.zerombr.zerombr and arch.isS390(): # run dasdfmt on any unformatted DASDs automatically threadMgr.add( AnacondaThread(name=constants.THREAD_DASDFMT, target=self.run_dasdfmt)) self._previous_autopart = False self._last_clicked_overview = None self._cur_clicked_overview = None self._grabObjects() def _grabObjects(self): self._customPart = self.builder.get_object("customRadioButton") self._encrypted = self.builder.get_object("encryptionCheckbox") self._reclaim = self.builder.get_object("reclaimCheckbox") def apply(self): applyDiskSelection(self.storage, self.data, self.selected_disks) self.data.autopart.autopart = self.autopart self.data.autopart.type = self.autoPartType self.data.autopart.encrypted = self.encrypted self.data.autopart.passphrase = self.passphrase if self.data.bootloader.bootDrive and \ self.data.bootloader.bootDrive not in self.selected_disks: self.data.bootloader.bootDrive = "" self.storage.bootloader.reset() self.data.clearpart.initAll = True if not self.autopart_missing_passphrase: self.clearPartType = CLEARPART_TYPE_NONE self.data.clearpart.type = self.clearPartType self.storage.config.update(self.data) self.storage.autoPartType = self.data.autopart.type self.storage.encryptedAutoPart = self.data.autopart.encrypted self.storage.encryptionPassphrase = self.data.autopart.passphrase # If autopart is selected we want to remove whatever has been # created/scheduled to make room for autopart. # If custom is selected, we want to leave alone any storage layout the # user may have set up before now. self.storage.config.clearNonExistent = self.data.autopart.autopart @gtk_action_nowait def execute(self): # Spawn storage execution as a separate thread so there's no big delay # going back from this spoke to the hub while StorageChecker.run runs. # Yes, this means there's a thread spawning another thread. Sorry. threadMgr.add( AnacondaThread(name=constants.THREAD_EXECUTE_STORAGE, target=self._doExecute)) def _doExecute(self): self._ready = False hubQ.send_not_ready(self.__class__.__name__) # on the off-chance dasdfmt is running, we can't proceed further threadMgr.wait(constants.THREAD_DASDFMT) hubQ.send_message(self.__class__.__name__, _("Saving storage configuration...")) if flags.automatedInstall and self.data.autopart.encrypted and not self.data.autopart.passphrase: self.autopart_missing_passphrase = True StorageChecker.errors = [ _("Passphrase for autopart encryption not specified.") ] self._ready = True hubQ.send_ready(self.__class__.__name__, True) return try: doKickstartStorage(self.storage, self.data, self.instclass) except (StorageError, KickstartParseError) as e: log.error("storage configuration failed: %s", e) StorageChecker.errors = str(e).split("\n") hubQ.send_message(self.__class__.__name__, _("Failed to save storage configuration...")) self.data.bootloader.bootDrive = "" self.data.ignoredisk.drives = [] self.data.ignoredisk.onlyuse = [] self.storage.config.update(self.data) self.storage.reset() self.disks = getDisks(self.storage.devicetree) # now set ksdata back to the user's specified config applyDiskSelection(self.storage, self.data, self.selected_disks) except BootLoaderError as e: log.error("BootLoader setup failed: %s", e) StorageChecker.errors = str(e).split("\n") hubQ.send_message(self.__class__.__name__, _("Failed to save storage configuration...")) self.data.bootloader.bootDrive = "" else: if self.autopart or (flags.automatedInstall and (self.data.autopart.autopart or self.data.partition.seen)): # run() executes StorageChecker.checkStorage in a seperate threat self.run() finally: resetCustomStorageData(self.data) self._ready = True hubQ.send_ready(self.__class__.__name__, True) @property def completed(self): retval = (threadMgr.get(constants.THREAD_EXECUTE_STORAGE) is None and threadMgr.get(constants.THREAD_CHECK_STORAGE) is None and self.storage.rootDevice is not None and not self.errors) return retval @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready @property def showable(self): return not flags.dirInstall @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") if flags.automatedInstall and not self.storage.rootDevice: msg = _("Kickstart insufficient") elif threadMgr.get(constants.THREAD_DASDFMT): msg = _("Formatting DASDs") elif self.data.ignoredisk.onlyuse: msg = P_(("%d disk selected"), ("%d disks selected"), len(self.data.ignoredisk.onlyuse)) % len( self.data.ignoredisk.onlyuse) if self.errors: msg = _("Error checking storage configuration") elif self.warnings: msg = _("Warning checking storage configuration") elif self.data.autopart.autopart: msg = _("Automatic partitioning selected") else: msg = _("Custom partitioning selected") return msg @property def localOverviews(self): return self.local_disks_box.get_children() @property def advancedOverviews(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 not event.type 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.localOverviews advanced_overviews = self.advancedOverviews # 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.disks = getDisks(self.storage.devicetree) # synchronize our local data store with the global ksdata disk_names = [d.name for d in self.disks] self.selected_disks = [ d for d in self.data.ignoredisk.onlyuse if d in disk_names ] # unhide previously hidden disks so that they don't look like being # empty (because of all child devices hidden) self._unhide_disks() self.autopart = self.data.autopart.autopart self.autoPartType = self.data.autopart.type if self.autoPartType is None: self.autoPartType = AUTOPART_TYPE_LVM self.encrypted = self.data.autopart.encrypted self.passphrase = self.data.autopart.passphrase self._previous_autopart = self.autopart # First, remove all non-button children. for child in self.localOverviews + self.advancedOverviews: 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. for disk in filter(isLocalDisk, self.disks): # While technically local disks, zFCP devices are specialized # storage and should not be shown here. if disk.type is not "zfcp": self._add_disk_overview(disk, self.local_disks_box) # 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 name in self.data.ignoredisk.onlyuse: if name not in disk_names: continue obj = self.storage.devicetree.getDeviceByName(name, hidden=True) # since zfcp devices may be detected as local disks when added # manually, specifically check the disk type here to make sure # we won't accidentally bypass adding zfcp devices to the disk # overview if isLocalDisk(obj) and obj.type is not "zfcp": continue self._add_disk_overview(obj, self.specialized_disks_box) # update the selections in the ui for overview in self.localOverviews + self.advancedOverviews: name = overview.get_property("name") overview.set_chosen(name in self.selected_disks) # if encrypted is specified in kickstart, select the encryptionCheckbox in the GUI if self.encrypted: self._encrypted.set_active(True) self._customPart.set_active(not self.autopart) self._update_summary() if self.errors: self.set_warning( _("Error checking storage configuration. <a href=\"\">Click for details.</a>" )) elif self.warnings: self.set_warning( _("Warning checking storage configuration. <a href=\"\">Click for details.</a>" )) def initialize(self): NormalSpoke.initialize(self) self.local_disks_box = self.builder.get_object("local_disks_box") self.specialized_disks_box = self.builder.get_object( "specialized_disks_box") # Connect the viewport adjustments to the child widgets # See also https://bugzilla.gnome.org/show_bug.cgi?id=744721 localViewport = self.builder.get_object("localViewport") specializedViewport = self.builder.get_object("specializedViewport") self.local_disks_box.set_focus_hadjustment( localViewport.get_hadjustment()) self.specialized_disks_box.set_focus_hadjustment( specializedViewport.get_hadjustment()) mainViewport = self.builder.get_object("storageViewport") mainBox = self.builder.get_object("storageMainBox") mainBox.set_focus_vadjustment(mainViewport.get_vadjustment()) threadMgr.add( AnacondaThread(name=constants.THREAD_STORAGE_WATCHER, target=self._initialize)) def _add_disk_overview(self, disk, box): if disk.removable: kind = "drive-removable-media" else: kind = "drive-harddisk" if disk.serial: popup_info = "%s" % disk.serial else: popup_info = None # We don't want to display the whole huge WWID for a multipath device. # That makes the DO way too wide. if isinstance(disk, MultipathDevice): desc = disk.wwid.split(":") description = ":".join(desc[0:3]) + "..." + ":".join(desc[-4:]) elif isinstance(disk, ZFCPDiskDevice): # 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)s\nWWPN %(wwpn)s\nLUN %(lun)s") % \ {"hba_id": disk.hba_id, "wwpn": disk.wwpn, "lun": disk.fcp_lun} else: description = disk.description free = self.storage.getFreeSpace(disks=[disk])[disk.name][0] overview = AnacondaWidgets.DiskOverview(description, kind, str(disk.size), _("%s free") % free, disk.name, popup=popup_info) box.pack_start(overview, False, False, 0) # FIXME: this will need to get smarter # # maybe a little function that resolves each item in onlyuse using # udev_resolve_devspec and compares that to the DiskDevice? overview.set_chosen(disk.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): hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PROBING_STORAGE)) threadMgr.wait(constants.THREAD_STORAGE) threadMgr.wait(constants.THREAD_CUSTOM_STORAGE_INIT) self.disks = getDisks(self.storage.devicetree) # if there's only one disk, select it by default if len(self.disks) == 1 and not self.selected_disks: applyDiskSelection(self.storage, self.data, [self.disks[0].name]) self._ready = True hubQ.send_ready(self.__class__.__name__, False) def _update_summary(self): """ Update the summary based on the UI. """ count = 0 capacity = Size(0) free = Size(0) # pass in our disk list so hidden disks' free space is available free_space = self.storage.getFreeSpace(disks=self.disks) selected = [d for d in self.disks if d.name in self.selected_disks] for disk in selected: capacity += disk.size free += free_space[disk.name][0] count += 1 anySelected = count > 0 summary = (P_( "%(count)d disk selected; %(capacity)s capacity; %(free)s free", "%(count)d disks selected; %(capacity)s capacity; %(free)s free", count) % { "count": count, "capacity": capacity, "free": free }) summary_label = self.builder.get_object("summary_label") summary_label.set_text(summary) summary_label.set_sensitive(anySelected) # 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( anySelected) self.builder.get_object( "local_untouched_label_revealer").set_reveal_child(anySelected) self.builder.get_object( "special_untouched_label_revealer").set_reveal_child(anySelected) self.builder.get_object("other_options_label").set_sensitive( anySelected) self.builder.get_object("other_options_grid").set_sensitive( anySelected) if len(self.disks) == 0: self.set_warning( _("No disks detected. Please shut down the computer, connect at least one disk, and restart to complete installation." )) elif not anySelected: self.set_warning( _("No disks selected; please select at least one disk to install to." )) else: self.clear_info() def _update_disk_list(self): """ Update self.selected_disks based on the UI. """ for overview in self.localOverviews + self.advancedOverviews: 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) def run_dasdfmt(self): """ Though the same function exists in pyanaconda.ui.gui.spokes.lib.dasdfmt, this instance doesn't include any of the UI pieces and should only really be getting called on ks installations with "zerombr". """ # wait for the initial storage thread to complete before taking any new # actions on storage devices threadMgr.wait(constants.THREAD_STORAGE) to_format = self.storage.devicetree.make_unformatted_dasd_list( d for d in getDisks(self.storage.devicetree)) if not to_format: # nothing to do here; bail return hubQ.send_message(self.__class__.__name__, _("Formatting DASDs")) for disk in to_format: try: blockdev.s390.dasd_format(disk.name) except blockdev.S390Error as err: # Log errors if formatting fails, but don't halt the installer log.error(str(err)) continue # signal handlers def on_summary_clicked(self, button): # show the selected disks dialog # pass in our disk list so hidden disks' free space is available free_space = self.storage.getFreeSpace(disks=self.disks) dialog = SelectedDisksDialog(self.data, ) dialog.refresh( [d for d in self.disks if d.name in self.selected_disks], free_space) self.run_lightbox_dialog(dialog) # update selected disks since some may have been removed self.selected_disks = [d.name for d in dialog.disks] # update the UI to reflect changes to self.selected_disks for overview in self.localOverviews + self.advancedOverviews: name = overview.get_property("name") overview.set_chosen(name in self.selected_disks) self._update_summary() self.data.bootloader.seen = True if self.data.bootloader.location == "none": 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 _setup_passphrase(self): dialog = PassphraseDialog(self.data) rc = self.run_lightbox_dialog(dialog) if rc != 1: return False self.passphrase = dialog.passphrase for device in self.storage.devices: if device.format.type == "luks" and not device.format.exists: if not device.format.hasKey: device.format.passphrase = self.passphrase return True def _remove_nonexistant_partitions(self): for partition in self.storage.partitions[:]: # check if it's been removed in a previous iteration if not partition.exists and \ partition in self.storage.partitions: self.storage.recursiveRemove(partition) def _hide_disks(self): for disk in self.disks: if disk.name not in self.selected_disks and \ disk in self.storage.devices: self.storage.devicetree.hide(disk) def _unhide_disks(self): if self._last_selected_disks: for disk in self.disks: if disk.name not in self.selected_disks and \ disk.name not in self._last_selected_disks: self.storage.devicetree.unhide(disk) def _check_dasd_formats(self): rc = DASD_FORMAT_NO_CHANGE dasds = self.storage.devicetree.make_unformatted_dasd_list( self.storage.devicetree.dasd) if len(dasds) > 0: # We want to apply current selection before running dasdfmt to # prevent this information from being lost afterward applyDiskSelection(self.storage, self.data, self.selected_disks) dialog = DasdFormatDialog(self.data, self.storage, dasds) ignoreEscape(dialog.window) rc = self.run_lightbox_dialog(dialog) return rc def _check_space_and_get_dialog(self, disks): # Figure out if the existing disk labels will work on this platform # you need to have at least one of the platform's labels in order for # any of the free space to be useful. disk_labels = set(disk.format.labelType for disk in disks if hasattr(disk.format, "labelType")) platform_labels = set(platform.diskLabelTypes) if disk_labels and platform_labels.isdisjoint(disk_labels): disk_free = 0 fs_free = 0 log.debug("Need disklabel: %s have: %s", ", ".join(platform_labels), ", ".join(disk_labels)) else: free_space = self.storage.getFreeSpace( disks=disks, clearPartType=CLEARPART_TYPE_NONE) disk_free = sum(f[0] for f in free_space.values()) fs_free = sum(f[1] for f in free_space.values()) disks_size = sum((d.size for d in disks), Size(0)) required_space = self.payload.spaceRequired auto_swap = sum((r.size for r in self.storage.autoPartitionRequests if r.fstype == "swap"), Size(0)) if self.autopart and auto_swap == Size(0): # autopartitioning requested, but not applied yet (=> no auto swap # requests), ask user for enough space to fit in the suggested swap auto_swap = autopart.swapSuggestion() log.debug("disk free: %s fs free: %s sw needs: %s auto swap: %s", disk_free, fs_free, required_space, auto_swap) # compare only to 90% of disk space because fs takes some space for a metadata if (disk_free * 0.9) >= required_space + auto_swap: dialog = None elif (disks_size * 0.9) >= required_space: dialog = NeedSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, auto_swap, disk_free, fs_free) else: dialog = NoSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, auto_swap, disk_free, fs_free) # the 'dialog' variable is always set by the if statement above return dialog def _run_dialogs(self, disks, start_with): rc = self.run_lightbox_dialog(start_with) if rc == RESPONSE_RECLAIM: # we need to run another dialog # respect disk selection and other choices in the ReclaimDialog self.apply() resize_dialog = ResizeDialog(self.data, self.storage, self.payload) resize_dialog.refresh(disks) return self._run_dialogs(disks, start_with=resize_dialog) else: # we are done return rc def on_back_clicked(self, button): # We can't exit early if it looks like nothing has changed because the # user might want to change settings presented in the dialogs shown from # within this method. # Do not enter this method multiple times if user clicking multiple times # on back button if self._back_clicked: return else: self._back_clicked = True # make sure the snapshot of unmodified on-disk-storage model is created if not on_disk_storage.created: on_disk_storage.create_snapshot(self.storage) if self.autopart_missing_passphrase: self._setup_passphrase() NormalSpoke.on_back_clicked(self, button) return # 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 disk_selection_changed = False if self._last_selected_disks: disk_selection_changed = (self._last_selected_disks != set( self.selected_disks)) # remember the disk selection for future decisions self._last_selected_disks = set(self.selected_disks) if disk_selection_changed: # 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. on_disk_storage.reset_to_snapshot(self.storage) self.disks = getDisks(self.storage.devicetree) else: # Remove all non-existing devices if autopart was active when we last # refreshed. if self._previous_autopart: self._previous_autopart = False self._remove_nonexistant_partitions() # hide disks as requested self._hide_disks() # make sure no containers were split up by the user's disk selection self.clear_info() # 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 self.disks_errors = checkDiskSelection(self.storage, self.selected_disks) if self.disks_errors: # The disk selection has to make sense before we can proceed. self.set_error( _("There was a problem with your disk selection. " "Click here for details.")) self._back_clicked = False return if arch.isS390(): # check for unformatted DASDs and launch dasdfmt if any discovered 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 # even if they're not doing autopart, setting autopart.encrypted # establishes a default of encrypting new devices self.encrypted = self._encrypted.get_active() # We might first need to ask about an encryption passphrase. if self.encrypted and not self._setup_passphrase(): self._back_clicked = False return # At this point there are three possible states: # 1) user chose custom part => just send them to the CustomPart spoke # 2) user wants to reclaim some more space => run the ResizeDialog # 3) we are just asked to do autopart => check free space and see if we need # user to do anything more self.autopart = not self._customPart.get_active() disks = [d for d in self.disks if d.name in self.selected_disks] dialog = None if not self.autopart: self.skipTo = "CustomPartitioningSpoke" elif self._reclaim.get_active(): # HINT: change the logic of this 'if' statement if we are asked to # support "reclaim before custom partitioning" # respect disk selection and other choices in the ReclaimDialog self.apply() dialog = ResizeDialog(self.data, self.storage, self.payload) dialog.refresh(disks) else: dialog = self._check_space_and_get_dialog(disks) if dialog: # more dialogs may need to be run based on user choices, but we are # only interested in the final result rc = self._run_dialogs(disks, start_with=dialog) if rc == RESPONSE_OK: # nothing special needed pass elif 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 elif rc == RESPONSE_MODIFY_SW: # The "Fedora software selection" link was clicked on one of the # dialogs. Send the user to the software spoke. self.skipTo = "SoftwareSelectionSpoke" elif 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") else: # 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 if self.autopart: refreshAutoSwapSize(self.storage) self.applyOnSkip = True NormalSpoke.on_back_clicked(self, button) def on_custom_toggled(self, button): # The custom button won't be active until after this handler is run, # so we have to negate everything here. self._reclaim.set_sensitive(not button.get_active()) if self._reclaim.get_sensitive(): self._reclaim.set_has_tooltip(False) else: self._reclaim.set_tooltip_text( _("You'll be able to make space available during custom partitioning." )) def on_specialized_clicked(self, button): # there will be changes in disk selection, revert storage to an early snapshot (if it exists) if on_disk_storage.created: on_disk_storage.reset_to_snapshot(self.storage) # 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. applyDiskSelection(self.storage, self.data, 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. iutil.ipmi_report(constants.IPMI_ABORTED) 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. iutil.ipmi_report(constants.IPMI_ABORTED) 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.localOverviews elif box is self.specialized_disks_box: overviews = self.advancedOverviews else: # no other box contains disk overviews return for overview in overviews: overview.set_chosen(True) self._update_disk_list()
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" focusWidgetName = "pw" uiFile = "spokes/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._kickstarted = False def initialize(self): NormalSpoke.initialize(self) # place holders for the text boxes self.pw = self.builder.get_object("pw") self.confirm = self.builder.get_object("confirmPW") # 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? # - Is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # The password confirmation needs to be checked whenever either of the password # fields change. Separate checks are created for each field so that edits on either # will trigger a new check and so that the last edited field will get focus when # Done is clicked. The checks are saved here so that either check can trigger the # other check in order to reset the status on both when either field is changed. # The check_data field is used as a flag to prevent infinite recursion. self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) self._password_check = self.add_check(self.pw, self._checkPasswordConfirm) # Keep a reference for these checks, since they have to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) self.add_check(self.confirm, self._checkPasswordEmpty) # Counters for checks that ask the user to click Done to confirm self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 # Password validation data self._pw_error_message = None self._pw_score = 0 self._kickstarted = self.data.rootpw.seen if self._kickstarted: self.pw.set_placeholder_text(_("The password is set.")) self.confirm.set_placeholder_text(_("The password is set.")) self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("root") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.pw.grab_focus() self.pw.emit("changed") self.confirm.emit("changed") @property def status(self): if self.data.rootpw.password: return _("Root password is set") elif self.data.rootpw.lock: return _("Root account is disabled") 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.pw.get_text() # value from the kickstart changed self.data.rootpw.seen = False self._kickstarted = False self.data.rootpw.lock = False if not pw: self.data.rootpw.password = '' self.data.rootpw.isCrypted = False return self.data.rootpw.password = cryptPassword(pw) self.data.rootpw.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") @property def completed(self): return bool(self.data.rootpw.password or self.data.rootpw.lock) @property def sensitive(self): return not (self.completed and flags.automatedInstall and self.data.rootpw.seen) def _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all.""" # If the password was set by kickstart, skip this check if self._kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK if not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """Check whether the password matches the confirmation data.""" pw = self.pw.get_text() confirm = self.confirm.get_text() # Skip the check if no password is required if (not pw and not confirm) and self._kickstarted: result = InputCheck.CHECK_OK elif confirm and (pw != confirm): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK # If the check succeeded, reset the status of the other check object # Disable the current check to prevent a cycle inputcheck.enabled = False if result == InputCheck.CHECK_OK: if inputcheck == self._confirm_check: self._password_check.update_check_status() else: self._confirm_check.update_check_status() inputcheck.enabled = True return result def _updatePwQuality(self, editable=None, data=None): """Update the password quality information. This function is called by the ::changed signal handler on the password field. """ pwtext = self.pw.get_text() # Reset the counters used for the "press Done twice" logic self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self._pw_score, status_text, _pw_quality, self._pw_error_message = validatePassword( pwtext, "root", minlen=self.policy.minlen, empty_ok=self.policy.emptyok) self.pw_bar.set_value(self._pw_score) self.pw_label.set_text(status_text) def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. Convert the strength set by _updatePwQuality into an error message. """ pw = self.pw.get_text() confirm = self.confirm.get_text() # Skip the check if no password is required if (not pw and not confirm) and self._kickstarted: return InputCheck.CHECK_OK # Check for validity errors # pw score == 0 & errors from libpwquality # - ignore if the strict flag in the password policy == False if not self._pw_score and self._pw_error_message and self.policy.strict: return self._pw_error_message # use strength from policy, not bars _pw_score, _status_text, pw_quality, _error_message = validatePassword( pw, "root", minlen=self.policy.minlen, empty_ok=self.policy.emptyok) if pw_quality < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK elif self._waiveStrengthClicks == 1: if self._pw_error_message: return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR ) % self._pw_error_message else: return _(PASSWORD_WEAK_CONFIRM) else: # non-strict allows done to be clicked twice if self.policy.strict: done_msg = "" else: done_msg = _(PASSWORD_DONE_TWICE) if self._pw_error_message: return _(PASSWORD_WEAK_WITH_ERROR ) % self._pw_error_message + " " + done_msg else: return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK def _checkPasswordASCII(self, inputcheck): """Set an error message if the password contains non-ASCII characters. Like the password strength check, this check can be bypassed by pressing Done twice. """ # If Done has been clicked, waive the check if self._waiveASCIIClicks > 0: return InputCheck.CHECK_OK password = self.get_input(inputcheck.input_obj) if password and any(char not in PW_ASCII_CHARS for char in password): return _(PASSWORD_ASCII) return InputCheck.CHECK_OK def on_back_clicked(self, button): # If the failed check is for password strength or non-ASCII # characters, add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: self._waiveASCIIClicks += 1 self._pwASCIICheck.update_check_status() # If neither the password nor the confirm field are set, skip the checks if (not self.pw.get_text()) and (not self.confirm.get_text()): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
RAID_NOT_ENOUGH_DISKS = N_("The RAID level you have selected (%(level)s) " "requires more disks (%(min)d) than you " "currently have selected (%(count)d).") 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:")) } # These cannot be specified as mountpoints system_mountpoints = ["/dev", "/proc", "/run", "/sys"] def size_from_entry(entry, lower_bound=None, units=None): """ Get a Size object from an entry field.
class SoftwareSelectionSpoke(NormalSpoke): builderObjects = ["addonStore", "environmentStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software.glade" 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): NormalSpoke.__init__(self, *args, **kwargs) self._errorMsgs = None self._tx_id = None self._selectFlag = False self.selectedGroups = [] self.excludedGroups = [] self.environment = None self._environmentListBox = self.builder.get_object("environmentListBox") self._addonListBox = self.builder.get_object("addonListBox") # Used to determine which add-ons to display for each environment. # The dictionary keys are environment IDs. The dictionary values are two-tuples # consisting of lists of add-on group IDs. The first list is the add-ons specific # to the environment, and the second list is the other add-ons possible for the # environment. self._environmentAddons = {} # 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 = {} # Used for detecting whether anything's changed in the spoke. self._origAddons = [] self._origEnvironment = None def _apply(self): env = self._get_selected_environment() if not env: return addons = self._get_selected_addons() for group in addons: if group not in self.selectedGroups: self.selectedGroups.append(group) self._selectFlag = False self.payload.data.packages.groupList = [] self.payload.selectEnvironment(env) self.environment = env for group in self.selectedGroups: 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() self.data.packages.seen = True def checkSoftwareSelection(self): from pyanaconda.packaging import DependencyError hubQ.send_message(self.__class__.__name__, _("Checking software dependencies...")) try: self.payload.checkSoftwareSelection() except DependencyError as e: self._errorMsgs = "\n".join(sorted(e.message)) 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 = not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and \ not self._errorMsgs and self.txid_valid if flags.automatedInstall: return processingDone and self.data.packages.seen else: return self._get_selected_environment() is not None and processingDone @property def changed(self): env = self._get_selected_environment() if not env: return True addons = self._get_selected_addons() # Don't redo dep solving if nothing's changed. if env == 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_MD) and not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and self.payload.baseRepo is not None) @property def showable(self): return not flags.livecdInstall and not self.data.method.method == "liveimg" @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") env = self._get_selected_environment() if not env: # Kickstart installs with %packages will have a row selected, unless # they did an install without a desktop environment. This should # catch that one case. if flags.automatedInstall and self.data.packages.seen: return _("Custom software selected") return _("Nothing selected") return self.payload.environmentDescription(env)[0] def initialize(self): NormalSpoke.initialize(self) threadMgr.add(AnacondaThread(name=constants.THREAD_SOFTWARE_WATCHER, target=self._initialize)) def _initialize(self): hubQ.send_message(self.__class__.__name__, _("Downloading package metadata...")) threadMgr.wait(constants.THREAD_PAYLOAD) hubQ.send_message(self.__class__.__name__, _("Downloading group metadata...")) # we have no way to select environments with kickstart right now # so don't try. if flags.automatedInstall and self.data.packages.seen: # We don't want to do a full refresh, just join the metadata thread threadMgr.wait(constants.THREAD_PAYLOAD_MD) else: # Grabbing the list of groups could potentially take a long time # at first (yum does a lot of magic property stuff, some of which # involves side effects like network access. We need to reference # them here, outside of the main thread, to not block the UI. try: # pylint: disable-msg=W0104 self.payload.environments # pylint: disable-msg=W0104 self.payload.groups # Parse the environments and groups into the form we want self._parseEnvironments() except MetadataError: hubQ.send_message(self.__class__.__name__, _("No installation source available")) return # And then having done all that 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 self.payload.release() 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. self._apply() def _parseEnvironments(self): self._environmentAddons = {} for environment in self.payload.environments: self._environmentAddons[environment] = ([], []) # Determine which groups are specific to this environment and which other groups # are available in this environment. for grp in self.payload.groups: if self.payload.environmentHasOption(environment, grp): self._environmentAddons[environment][0].append(grp) elif self.payload._isGroupVisible(grp) and self.payload._groupHasInstallableMembers(grp): self._environmentAddons[environment][1].append(grp) # Set all of the add-on selection states to the default self._addonStates = {} for grp in self.payload.groups: self._addonStates[grp] = self._ADDON_DEFAULT @gtk_action_wait def _first_refresh(self): try: self.refresh() return True except MetadataError: hubQ.send_message(self.__class__.__name__, _("No installation source available")) return False def _add_row(self, listbox, name, desc, button): row = Gtk.ListBoxRow() box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) box.set_spacing(6) button.set_valign(Gtk.Align.START) button.connect("clicked", self.on_button_toggled, row) box.add(button) label = Gtk.Label() label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) label.set_markup("<b>%s</b>\n%s" % (escape_markup(name), escape_markup(desc))) label.set_hexpand(True) label.set_alignment(0, 0.5) box.add(label) row.add(box) listbox.insert(row, -1) def refresh(self): NormalSpoke.refresh(self) threadMgr.wait(constants.THREAD_PAYLOAD_MD) if self.environment not in self.payload.environments: self.environment = None firstEnvironment = True firstRadio = None self._clear_listbox(self._environmentListBox) for environment in self.payload.environments: (name, desc) = self.payload.environmentDescription(environment) radio = Gtk.RadioButton(group=firstRadio) active = environment == self.environment or \ not self.environment and firstEnvironment radio.set_active(active) if active: self.environment = environment self._add_row(self._environmentListBox, name, desc, radio) firstRadio = firstRadio or radio firstEnvironment = False self.refreshAddons() def _addAddon(self, grp): (name, desc) = self.payload.groupDescription(grp) # 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.environment, grp) check = Gtk.CheckButton() check.set_active(selected) self._add_row(self._addonListBox, name, desc, check) def refreshAddons(self): # The source was changed, make sure the list is current if not self.txid_valid: self._parseEnvironments() if self.environment and (self.environment in self._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. addSep = len(self._environmentAddons[self.environment][0]) > 0 and \ len(self._environmentAddons[self.environment][1]) > 0 for grp in self._environmentAddons[self.environment][0]: self._addAddon(grp) # This marks a separator in the view - only add it if there's both environment # specific and generic addons. if addSep: self._addonListBox.insert(Gtk.Separator(), -1) for grp in self._environmentAddons[self.environment][1]: self._addAddon(grp) self._selectFlag = True if self._errorMsgs: self.set_warning(_("Error checking software dependencies. Click for details.")) else: self.clear_info() def _allAddons(self): return self._environmentAddons[self.environment][0] + \ [""] + \ self._environmentAddons[self.environment][1] 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 # Returns the row in the store corresponding to what's selected on the # left hand panel, or None if nothing's selected. def _get_selected_environment(self): for (ndx, row) in enumerate(self._environmentListBox.get_children()): box = row.get_children()[0] button = box.get_children()[0] if button.get_active(): return self.payload.environments[ndx] return None 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_button_toggled(self, radio, row): row.activate() def on_environment_activated(self, listbox, row): if not self._selectFlag: return box = row.get_children()[0] button = box.get_children()[0] button.handler_block_by_func(self.on_button_toggled) button.set_active(not button.get_active()) button.handler_unblock_by_func(self.on_button_toggled) # Remove all the groups that were selected by the previously # selected environment. for groupid in self.payload.environmentGroups(self.environment): if groupid in self.selectedGroups: self.selectedGroups.remove(groupid) # Then 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_addon_activated(self, listbox, row): box = row.get_children()[0] if isinstance(box, Gtk.Separator): return button = box.get_children()[0] addons = self._allAddons() group = addons[row.get_index()] wasActive = group in self.selectedGroups button.handler_block_by_func(self.on_button_toggled) button.set_active(not wasActive) button.handler_unblock_by_func(self.on_button_toggled) if wasActive: self.selectedGroups.remove(group) self._addonStates[group] = self._ADDON_DESELECTED else: self.selectedGroups.append(group) if group in self.excludedGroups: self.excludedGroups.remove(group) self._addonStates[group] = self._ADDON_SELECTED def on_info_bar_clicked(self, *args): if not self._errorMsgs: return label = _("The following software marked for installation has errors. " "This is likely caused by an error with\nyour installation source. " "You can change your installation source or quit the installer.") dialog = DetailedErrorDialog(self.data, buttons=[C_("GUI|Software Selection|Error Dialog", "_Quit"), C_("GUI|Software Selection|Error Dialog", "_Cancel"), C_("GUI|Software Selection|Error Dialog", "_Modify Software Source")], label=label) with enlightbox(self.window, dialog.window): dialog.refresh(self._errorMsgs) rc = dialog.run() dialog.window.destroy() if rc == 0: # Quit. sys.exit(0) elif rc == 1: # Close the dialog so the user can change selections. pass elif rc == 2: # Send the user to the installation source spoke. self.skipTo = "SourceSpoke" self.window.emit("button-clicked") else: pass
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" uiFile = "spokes/password.glade" category = UserSettingsCategory icon = "dialog-password-symbolic" title = CN_("GUI|Spoke", "_ROOT PASSWORD") def __init__(self, *args): NormalSpoke.__init__(self, *args) GUISpokeInputCheckHandler.__init__(self) self._kickstarted = False def initialize(self): NormalSpoke.initialize(self) # place holders for the text boxes self.pw = self.builder.get_object("pw") self.confirm = self.builder.get_object("confirmPW") # 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? # - Is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # The password confirmation needs to be checked whenever either of the password # fields change. Separate checks are created for each field so that edits on either # will trigger a new check and so that the last edited field will get focus when # Done is clicked. The checks are saved here so that either check can trigger the # other check in order to reset the status on both when either field is changed. # The check_data field is used as a flag to prevent infinite recursion. self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) self._password_check = self.add_check(self.pw, self._checkPasswordConfirm) # Keep a reference for this check, since it has to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self.add_check(self.confirm, self._checkPasswordEmpty) # Counter for the click Done twice check override self._waivePasswordClicks = 0 # Password validation data self._pwq_error = None self._pwq_valid = True self._kickstarted = self.data.rootpw.seen if self._kickstarted: self.pw.set_placeholder_text(_("The password is set.")) self.confirm.set_placeholder_text(_("The password is set.")) self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.pw.grab_focus() self.pw.emit("changed") self.confirm.emit("changed") @property def status(self): if self.data.rootpw.password: return _("Root password is set") elif self.data.rootpw.lock: return _("Root account is disabled") 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.pw.get_text() if not pw: return self.data.rootpw.password = cryptPassword(pw) self.data.rootpw.isCrypted = True self.data.rootpw.lock = False # value from the kickstart changed self.data.rootpw.seen = False self._kickstarted = False self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") @property def completed(self): return bool(self.data.rootpw.password or self.data.rootpw.lock) def _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all.""" # If the password was set by kickstart, skip this check if self._kickstarted: return InputCheck.CHECK_OK if not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """Check whether the password matches the confirmation data.""" pw = self.pw.get_text() confirm = self.confirm.get_text() # Skip the check if no password is required if (not pw and not confirm) and self._kickstarted: result = InputCheck.CHECK_OK elif confirm and (pw != confirm): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK # If the check succeeded, reset the status of the other check object # Disable the current check to prevent a cycle inputcheck.enabled = False if result == InputCheck.CHECK_OK: if inputcheck == self._confirm_check: self._password_check.update_check_status() else: self._confirm_check.update_check_status() inputcheck.enabled = True return result def _updatePwQuality(self, editable=None, data=None): """Update the password quality information. This function is called by the ::changed signal handler on the password field. """ pwtext = self.pw.get_text() # Reset the counter used for the "press Done twice" logic self._waivePasswordClicks = 0 self._pwq_valid, strength, self._pwq_error = validatePassword( pwtext, "root") if not pwtext: val = 0 elif strength < 50: val = 1 elif strength < 75: val = 2 elif strength < 90: val = 3 else: val = 4 text = _(PASSWORD_STRENGTH_DESC[val]) self.pw_bar.set_value(val) self.pw_label.set_text(text) def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. Convert the strength set by _updatePwQuality into an error message. """ pw = self.pw.get_text() confirm = self.confirm.get_text() # Skip the check if no password is required if (not pw and not confirm) and self._kickstarted: return InputCheck.CHECK_OK # Check for validity errors if (not self._pwq_valid) and (self._pwq_error): return self._pwq_error pwstrength = self.pw_bar.get_value() if pwstrength < 2: # If Done has been clicked twice, waive the check if self._waivePasswordClicks > 1: return InputCheck.CHECK_OK elif self._waivePasswordClicks == 1: if self._pwq_error: return _( PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error else: return _(PASSWORD_WEAK_CONFIRM) else: if self._pwq_error: return _(PASSWORD_WEAK_WITH_ERROR) % self._pwq_error else: return _(PASSWORD_WEAK) else: return InputCheck.CHECK_OK def on_back_clicked(self, button): # Add a click and re-check the password strength self._waivePasswordClicks += 1 self._pwStrengthCheck.update_check_status() # If neither the password nor the confirm field are set, skip the checks if (not self.pw.get_text()) and (not self.confirm.get_text()): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
class SoftwareSelectionSpoke(NormalSpoke): """ .. inheritance-diagram:: SoftwareSelectionSpoke :parts: 3 """ builderObjects = ["addonStore", "environmentStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software.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): NormalSpoke.__init__(self, *args, **kwargs) self._errorMsgs = None self._tx_id = None self._selectFlag = False self.selectedGroups = [] self.excludedGroups = [] 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( environmentViewport.get_vadjustment()) self._addonListBox.set_focus_vadjustment( addonViewport.get_vadjustment()) # 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. """ 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): if not self.environment: 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 set(addons) != set(self._origAddons): for group in addons: if group not in self.selectedGroups: self.selectedGroups.append(group) self._selectFlag = False self.payload.data.packages.packageList = [] self.payload.data.packages.groupList = [] self.payload.selectEnvironment(self.environment) for group in self.selectedGroups: 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.packaging 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 flags.automatedInstall and self.data.packages.seen: 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.data.packages.seen: # %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): NormalSpoke.initialize(self) threadMgr.add( AnacondaThread(name=constants.THREAD_SOFTWARE_WATCHER, target=self._initialize)) def _initialize(self): threadMgr.wait(constants.THREAD_PAYLOAD) 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() def _parseEnvironments(self): # Set all of the add-on selection states to the default self._addonStates = {} for grp in self.payload.groups: self._addonStates[grp] = self._ADDON_DEFAULT @gtk_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): NormalSpoke.refresh(self) 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 _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_checkbox_toggled(self, button, row): row.activate() 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) # Remove all the groups that were selected by the previously # selected environment. if self.environment: for groupid in self.payload.environmentGroups(self.environmentid): if groupid in self.selectedGroups: self.selectedGroups.remove(groupid) # Then 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_addon_activated(self, listbox, row): box = row.get_children()[0] if isinstance(box, Gtk.Separator): return # GUI selections means that packages are no longer coming from kickstart self._kickstarted = False button = box.get_children()[0] addons = self._allAddons() group = addons[row.get_index()] wasActive = group in self.selectedGroups with blockedHandler(button, self.on_checkbox_toggled): button.set_active(not wasActive) if wasActive: self.selectedGroups.remove(group) self._addonStates[group] = self._ADDON_DESELECTED else: self.selectedGroups.append(group) if group in self.excludedGroups: self.excludedGroups.remove(group) self._addonStates[group] = self._ADDON_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. iutil.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 PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: PasswordSpoke :parts: 3 """ builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" focusWidgetName = "pw" uiFile = "spokes/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._kickstarted = False def initialize(self): NormalSpoke.initialize(self) self.initialize_start() # place holders for the text boxes self.pw = self.builder.get_object("pw") self.confirm = self.builder.get_object("confirmPW") # 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? # - Is there any data in the confirm box? self._confirm_check = self.add_check(self.confirm, self.check_password_confirm) # Keep a reference for these checks, since they have to be manually run for the # click Done twice check. self._pwEmptyCheck = self.add_check(self.pw, self.check_password_empty) self._pwStrengthCheck = self.add_check(self.pw, self.check_user_password_strength) self._pwASCIICheck = self.add_check(self.pw, self.check_password_ASCII) self._kickstarted = self.data.rootpw.seen if self._kickstarted: self.pw.set_placeholder_text(_("The password is set.")) self.confirm.set_placeholder_text(_("The password is set.")) self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) self.pw_bar.add_offset_value("full", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("root") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() # set the visibility of the password entries set_password_visibility(self.pw, False) set_password_visibility(self.confirm, False) # 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): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.pw.grab_focus() self.pw.emit("changed") self.confirm.emit("changed") @property def status(self): if self.data.rootpw.password: return _("Root password is set") elif self.data.rootpw.lock: return _("Root account is disabled") 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.pw.get_text() # value from the kickstart changed self.data.rootpw.seen = False self._kickstarted = False self.data.rootpw.lock = False if not pw: self.data.rootpw.password = '' self.data.rootpw.isCrypted = False return self.data.rootpw.password = cryptPassword(pw) self.data.rootpw.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") # Send ready signal to main event loop hubQ.send_ready(self.__class__.__name__, False) @property def completed(self): return bool(self.data.rootpw.password or self.data.rootpw.lock) @property def sensitive(self): return not (self.completed and flags.automatedInstall and self.data.rootpw.seen) @property def input(self): return self.pw.get_text() @property def input_confirmation(self): return self.confirm.get_text() @property def input_kickstarted(self): return self.data.rootpw.seen @property def input_username(self): return "root" def set_input_score(self, score): self.pw_bar.set_value(score) def set_input_status(self, status_message): self.pw_label.set_text(status_message) def on_password_changed(self, editable, data=None): # Reset the counters used for the "press Done twice" logic self.waive_clicks = 0 self.waive_ASCII_clicks = 0 # Update the password/confirm match check on changes to the main password field self._confirm_check.update_check_status() 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): # If the failed check is for password strength or non-ASCII # characters, add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict: if failed_check == self._pwStrengthCheck: self.waive_clicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwEmptyCheck: self.waive_clicks += 1 self._pwEmptyCheck.update_check_status() elif failed_check: # no failed checks -> failed_check == None failed_check.update_check_status() elif failed_check == self._pwASCIICheck: self.waive_ASCII_clicks += 1 self._pwASCIICheck.update_check_status() if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
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) 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 self.password_kickstarted = self.data.rootpw.seen # 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.name_of_password = _(constants.NAME_OF_PASSWORD) self.checker.name_of_password_plural = _(constants.NAME_OF_PASSWORD_PLURAL) # 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() # regards both fields empty as success to let the user escape self._confirm_check.success_if_confirmation_empty = True # 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.data.rootpw.password: return _("Root password is set") elif self.data.rootpw.lock: return _("Root account is disabled") 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 self.data.rootpw.seen = False self.password_kickstarted = False self.data.rootpw.lock = False if not pw: self.data.rootpw.password = '' self.data.rootpw.isCrypted = False return # we have a password - set it to kickstart data self.data.rootpw.password = cryptPassword(pw) self.data.rootpw.isCrypted = True # 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.data.rootpw.password or self.data.rootpw.lock) @property def sensitive(self): return not (self.completed and flags.automatedInstall and self.data.rootpw.seen) 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.")
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) 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.name_of_password = _(constants.NAME_OF_PASSWORD) self.checker.name_of_password_plural = _(constants.NAME_OF_PASSWORD_PLURAL) self.checker.username = self.username # 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() # regards both fields empty as success to let the user escape self._confirm_check.success_if_confirmation_empty = True # 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.data.rootpw.password and not self.data.rootpw.lock 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 any root password. Separate logic makes sure an # empty string is not unexpectedly set as the user password. if 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 SourceSpoke(NormalSpoke): builderObjects = [ "isoChooser", "isoFilter", "partitionStore", "sourceWindow", "dirImage", "repoStore" ] mainWidgetName = "sourceWindow" uiFile = "spokes/source.glade" category = SoftwareCategory icon = "media-optical-symbolic" title = CN_("GUI|Spoke", "_INSTALLATION SOURCE") def __init__(self, *args, **kwargs): NormalSpoke.__init__(self, *args, **kwargs) self._currentIsoFile = None self._ready = False self._error = False self._proxyUrl = "" self._proxyChange = False self._cdrom = None def apply(self): # If askmethod was provided on the command line, entering the source # spoke wipes that out. if flags.askmethod: flags.askmethod = False threadMgr.add( AnacondaThread(name=constants.THREAD_PAYLOAD_MD, target=self.getRepoMetadata)) self.clear_info() def _method_changed(self): """ Check to see if the install method has changed. :returns: True if it changed, False if not :rtype: bool """ import copy old_source = copy.deepcopy(self.data.method) if self._autodetectButton.get_active(): if not self._cdrom: return False self.data.method.method = "cdrom" self.payload.install_device = self._cdrom if old_source.method == "cdrom": # XXX maybe we should always redo it for cdrom in case they # switched disks return False elif self._isoButton.get_active(): # If the user didn't select a partition (not sure how that would # happen) or didn't choose a directory (more likely), then return # as if they never did anything. part = self._get_selected_partition() if not part or not self._currentIsoFile: return False self.data.method.method = "harddrive" self.data.method.partition = part.name # The / gets stripped off by payload.ISOImage self.data.method.dir = "/" + self._currentIsoFile if (old_source.method == "harddrive" and old_source.partition == self.data.method.partition and old_source.dir == self.data.method.dir): return False # Make sure anaconda doesn't touch this device. part.protected = True self.storage.config.protectedDevSpecs.append(part.name) elif self._mirror_active(): # this preserves the url for later editing self.data.method.method = None self.data.method.proxy = self._proxyUrl if not old_source.method and self.payload.baseRepo and \ not self._proxyChange: return False elif self._http_active() or self._ftp_active(): url = self._urlEntry.get_text().strip() mirrorlist = False # If the user didn't fill in the URL entry, just return as if they # selected nothing. if url == "": return False # Make sure the URL starts with the protocol. yum will want that # to know how to fetch, and the refresh method needs that to know # which element of the combo to default to should this spoke be # revisited. if self._ftp_active() and not url.startswith("ftp://"): url = "ftp://" + url elif self._protocolComboBox.get_active( ) == PROTOCOL_HTTP and not url.startswith("http://"): url = "http://" + url mirrorlist = self._mirrorlistCheckbox.get_active() elif self._protocolComboBox.get_active( ) == PROTOCOL_HTTPS and not url.startswith("https://"): url = "https://" + url mirrorlist = self._mirrorlistCheckbox.get_active() if old_source.method == "url" and not self._proxyChange and \ ((not mirrorlist and old_source.url == url) or \ (mirrorlist and old_source.mirrorlist == url)): return False self.data.method.method = "url" self.data.method.proxy = self._proxyUrl if mirrorlist: self.data.method.mirrorlist = url self.data.method.url = "" else: self.data.method.url = url self.data.method.mirrorlist = "" elif self._nfs_active(): url = self._urlEntry.get_text().strip() # If the user didn't fill in the URL entry, or it does not contain # a ':' (so, no host/directory split), just return as if they # selected nothing. if url == "" or not ':' in url: return False self.data.method.method = "nfs" try: (self.data.method.server, self.data.method.dir) = url.split(":", 2) except ValueError as e: log.error("ValueError: %s", e) gtk_call_once( self.set_warning, _("Failed to set up installation source; check the repo url" )) self._error = True return self.data.method.opts = self.builder.get_object( "nfsOptsEntry").get_text() or "" if (old_source.method == "nfs" and old_source.server == self.data.method.server and old_source.dir == self.data.method.dir and old_source.opts == self.data.method.opts): return False # If the user moved from an HDISO method to some other, we need to # clear the protected bit on that device. if old_source.method == "harddrive" and old_source.partition: self._currentIsoFile = None self._isoChooserButton.set_label(self._origIsoChooserButton) self._isoChooserButton.set_use_underline(True) if old_source.partition in self.storage.config.protectedDevSpecs: self.storage.config.protectedDevSpecs.remove( old_source.partition) dev = self.storage.devicetree.getDeviceByName(old_source.partition) if dev: dev.protected = False self._proxyChange = False return True def getRepoMetadata(self): hubQ.send_not_ready("SoftwareSelectionSpoke") hubQ.send_not_ready(self.__class__.__name__) hubQ.send_message(self.__class__.__name__, _(BASEREPO_SETUP_MESSAGE)) # this sleep is lame, but without it the message above doesn't seem # to get processed by the hub in time, and is never shown. # FIXME this should get removed when we figure out how to ensure # that the message takes effect on the hub before we try to mount # a bad NFS server. time.sleep(1) try: self.payload.updateBaseRepo(fallback=False, checkmount=False) except (OSError, PayloadError) as e: log.error("PayloadError: %s", e) self._error = True hubQ.send_message(self.__class__.__name__, _("Failed to set up installation source")) if not (hasattr(self.data.method, "proxy") and self.data.method.proxy): gtk_call_once( self.set_warning, _("Failed to set up installation source; check the repo url" )) else: gtk_call_once( self.set_warning, _("Failed to set up installation source; check the repo url and proxy settings" )) else: self._error = False hubQ.send_message(self.__class__.__name__, _(METADATA_DOWNLOAD_MESSAGE)) self.payload.gatherRepoMetadata() self.payload.release() if not self.payload.baseRepo: hubQ.send_message(self.__class__.__name__, _(METADATA_ERROR_MESSAGE)) hubQ.send_ready(self.__class__.__name__, False) self._error = True gtk_call_once( self.set_warning, _("Failed to set up installation source; check the repo url" )) else: try: # Grabbing the list of groups could potentially take a long time the # first time (yum does a lot of magic property stuff, some of which # involves side effects like network access) so go ahead and grab # them now. These are properties with side-effects, just accessing # them will trigger yum. # pylint: disable=W0104 self.payload.environments # pylint: disable=W0104 self.payload.groups except MetadataError: hubQ.send_message("SoftwareSelectionSpoke", _("No installation source available")) else: hubQ.send_ready("SoftwareSelectionSpoke", False) finally: hubQ.send_ready(self.__class__.__name__, False) @property def changed(self): method_changed = self._method_changed() update_payload_repos = self._update_payload_repos() return method_changed or update_payload_repos or self._error @property def completed(self): """ WARNING: This can be called before _initialize is done, make sure that it doesn't access things that are not setup (eg. payload.*) until it is ready """ if flags.automatedInstall and self.ready and ( not self.data.method.method or not self.payload.baseRepo): return False else: return not self._error and self.ready and ( self.data.method.method or self.payload.baseRepo) @property def mandatory(self): return True @property def ready(self): return (self._ready and not threadMgr.get(constants.THREAD_PAYLOAD_MD) and not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and not threadMgr.get(constants.THREAD_CHECK_SOFTWARE)) @property def status(self): if threadMgr.get(constants.THREAD_CHECK_SOFTWARE): return _("Checking software dependencies...") elif not self.ready: return _(BASEREPO_SETUP_MESSAGE) elif not self.payload.baseRepo: return _("Error setting up base repository") elif self._error: return _("Error setting up software source") elif self.data.method.method == "url": return self.data.method.url or self.data.method.mirrorlist elif self.data.method.method == "nfs": return _("NFS server %s") % self.data.method.server elif self.data.method.method == "cdrom": return _("Local media") elif self.data.method.method == "harddrive": if not self._currentIsoFile: return _("Error setting up ISO file") return os.path.basename(self._currentIsoFile) elif self.payload.baseRepo: return _("Closest mirror") else: return _("Nothing selected") def _grabObjects(self): self._autodetectButton = self.builder.get_object( "autodetectRadioButton") self._autodetectBox = self.builder.get_object("autodetectBox") self._autodetectDeviceLabel = self.builder.get_object( "autodetectDeviceLabel") self._autodetectLabel = self.builder.get_object("autodetectLabel") self._isoButton = self.builder.get_object("isoRadioButton") self._isoBox = self.builder.get_object("isoBox") self._networkButton = self.builder.get_object("networkRadioButton") self._networkBox = self.builder.get_object("networkBox") self._urlEntry = self.builder.get_object("urlEntry") self._protocolComboBox = self.builder.get_object("protocolComboBox") self._isoChooserButton = self.builder.get_object("isoChooserButton") self._origIsoChooserButton = self._isoChooserButton.get_label() self._mirrorlistCheckbox = self.builder.get_object( "mirrorlistCheckbox") self._noUpdatesCheckbox = self.builder.get_object("noUpdatesCheckbox") self._verifyIsoButton = self.builder.get_object("verifyIsoButton") # addon repo objects self._repoEntryBox = self.builder.get_object("repoEntryBox") self._repoStore = self.builder.get_object("repoStore") self._repoSelection = self.builder.get_object("repoSelection") self._repoNameEntry = self.builder.get_object("repoNameEntry") self._repoProtocolComboBox = self.builder.get_object( "repoProtocolComboBox") self._repoUrlEntry = self.builder.get_object("repoUrlEntry") self._repoMirrorlistCheckbox = self.builder.get_object( "repoMirrorlistCheckbox") self._repoProxyUrlEntry = self.builder.get_object("repoProxyUrlEntry") self._repoProxyUsernameEntry = self.builder.get_object( "repoProxyUsernameEntry") self._repoProxyPasswordEntry = self.builder.get_object( "repoProxyPasswordEntry") # updates option container self._updatesBox = self.builder.get_object("updatesBox") self._proxyButton = self.builder.get_object("proxyButton") self._nfsOptsBox = self.builder.get_object("nfsOptsBox") def initialize(self): NormalSpoke.initialize(self) self._grabObjects() # I shouldn't have to do this outside GtkBuilder, but it really doesn't # want to let me pass in user data. self._autodetectButton.connect("toggled", self.on_source_toggled, self._autodetectBox) self._isoButton.connect("toggled", self.on_source_toggled, self._isoBox) self._networkButton.connect("toggled", self.on_source_toggled, self._networkBox) # Show or hide the updates option based on the installclass if self.instclass.installUpdates: really_show(self._updatesBox) else: really_hide(self._updatesBox) self._repoNameWarningBox = self.builder.get_object( "repoNameWarningBox") self._repoNameWarningLabel = self.builder.get_object( "repoNameWarningLabel") self._repoNamesWarningBox = self.builder.get_object( "repoNamesWarningBox") self._repoNamesWarningLabel = self.builder.get_object( "repoNamesWarningLabel") threadMgr.add( AnacondaThread(name=constants.THREAD_SOURCE_WATCHER, target=self._initialize)) def _initialize(self): hubQ.send_message(self.__class__.__name__, _("Probing storage...")) threadMgr.wait(constants.THREAD_STORAGE) hubQ.send_message(self.__class__.__name__, _(METADATA_DOWNLOAD_MESSAGE)) threadMgr.wait(constants.THREAD_PAYLOAD) added = False # If there's no fallback mirror to use, we should just disable that option # in the UI. if not self.payload.mirrorEnabled: self._protocolComboBox.remove(PROTOCOL_MIRROR) # If we've previously set up to use a CD/DVD method, the media has # already been mounted by payload.setup. We can't try to mount it # again. So just use what we already know to create the selector. # Otherwise, check to see if there's anything available. if self.data.method.method == "cdrom": self._cdrom = self.payload.install_device elif not flags.automatedInstall: self._cdrom = opticalInstallMedia(self.storage.devicetree) if self._cdrom: fire_gtk_action(self._autodetectDeviceLabel.set_text, _("Device: %s") % self._cdrom.name) fire_gtk_action( self._autodetectLabel.set_text, _("Label: %s") % (getattr(self._cdrom.format, "label", "") or "")) added = True if self.data.method.method == "harddrive": self._currentIsoFile = self.payload.ISOImage # These UI elements default to not being showable. If optical install # media were found, mark them to be shown. if added: gtk_call_once(self._autodetectBox.set_no_show_all, False) gtk_call_once(self._autodetectButton.set_no_show_all, False) # Add the mirror manager URL in as the default for HTTP and HTTPS. # We'll override this later in the refresh() method, if they've already # provided a URL. # FIXME self._reset_repoStore() self._ready = True hubQ.send_ready(self.__class__.__name__, False) def refresh(self): NormalSpoke.refresh(self) # Find all hard drive partitions that could hold an ISO and add each # to the partitionStore. This has to be done here because if the user # has done partitioning first, they may have blown away partitions # found during _initialize on the partitioning spoke. store = self.builder.get_object("partitionStore") store.clear() added = False active = 0 idx = 0 for dev in potentialHdisoSources(self.storage.devicetree): # path model size format type uuid of format dev_info = { "model": self._sanitize_model(dev.disk.model), "path": dev.path, "size": dev.size, "format": dev.format.name or "", "label": dev.format.label or dev.format.uuid or "" } # With the label in here, the combo box can appear really long thus pushing the "pick an image" # and the "verify" buttons off the screen. if dev_info["label"] != "": dev_info["label"] = "\n" + dev_info["label"] store.append([ dev, "%(model)s %(path)s (%(size)s MB) %(format)s %(label)s" % dev_info ]) if self.data.method.method == "harddrive" and self.data.method.partition in [ dev.path, dev.name ]: active = idx added = True idx += 1 # Again, only display these widgets if an HDISO source was found. self._isoBox.set_no_show_all(not added) self._isoBox.set_visible(added) self._isoButton.set_no_show_all(not added) self._isoButton.set_visible(added) if added: combo = self.builder.get_object("isoPartitionCombo") combo.set_active(active) # We default to the mirror list, and then if the method tells us # something different later, we can change it. self._protocolComboBox.set_active(PROTOCOL_MIRROR) self._urlEntry.set_sensitive(False) # Set up the default state of UI elements. if self.data.method.method == "url": self._networkButton.set_active(True) proto = self.data.method.url or self.data.method.mirrorlist if proto.startswith("http:"): self._protocolComboBox.set_active(PROTOCOL_HTTP) l = 7 elif proto.startswith("https:"): self._protocolComboBox.set_active(PROTOCOL_HTTPS) l = 8 elif proto.startswith("ftp:"): self._protocolComboBox.set_active(PROTOCOL_FTP) l = 6 else: self._protocolComboBox.set_active(PROTOCOL_HTTP) l = 0 self._urlEntry.set_sensitive(True) self._urlEntry.set_text(proto[l:]) self._mirrorlistCheckbox.set_active( bool(self.data.method.mirrorlist)) self._proxyUrl = self.data.method.proxy elif self.data.method.method == "nfs": self._networkButton.set_active(True) self._protocolComboBox.set_active(PROTOCOL_NFS) self._urlEntry.set_text( "%s:%s" % (self.data.method.server, self.data.method.dir)) self._urlEntry.set_sensitive(True) self.builder.get_object("nfsOptsEntry").set_text( self.data.method.opts or "") elif self.data.method.method == "harddrive": self._isoButton.set_active(True) self._isoBox.set_sensitive(True) self._verifyIsoButton.set_sensitive(True) if self._currentIsoFile: self._isoChooserButton.set_label( os.path.basename(self._currentIsoFile)) else: self._isoChooserButton.set_label("") self._isoChooserButton.set_use_underline(False) else: # No method was given in advance, so now we need to make a sensible # guess. Go with autodetected media if that was provided, and then # fall back to closest mirror. if not self._autodetectButton.get_no_show_all(): self._autodetectButton.set_active(True) self.data.method.method = "cdrom" else: self._networkButton.set_active(True) self.data.method.method = None self._proxyUrl = self.data.method.proxy self._setup_no_updates() # Setup the addon repos self._reset_repoStore() # Then, some widgets get enabled/disabled/greyed out depending on # how others are set up. We can use the signal handlers to handle # that condition here too. self.on_protocol_changed(self._protocolComboBox) def _setup_no_updates(self): """ Setup the state of the No Updates checkbox. If closest mirror is not selected, check it. If closest mirror is selected, and "updates" repo is enabled, uncheck it. """ self._updatesBox.set_sensitive(self._mirror_active()) active = not self._mirror_active() or not self.payload.isRepoEnabled( "updates") self._noUpdatesCheckbox.set_active(active) @property def showable(self): return not flags.livecdInstall and not self.data.method.method == "liveimg" def _mirror_active(self): return self._protocolComboBox.get_active() == PROTOCOL_MIRROR def _http_active(self): return self._protocolComboBox.get_active() in [ PROTOCOL_HTTP, PROTOCOL_HTTPS ] def _ftp_active(self): return self._protocolComboBox.get_active() == PROTOCOL_FTP def _nfs_active(self): return self._protocolComboBox.get_active() == PROTOCOL_NFS def _get_selected_partition(self): store = self.builder.get_object("partitionStore") combo = self.builder.get_object("isoPartitionCombo") selected = combo.get_active() if selected == -1: return None else: return store[selected][0] def _sanitize_model(self, model): return model.replace("_", " ") # Signal handlers. def on_source_toggled(self, button, relatedBox): # When a radio button is clicked, this handler gets called for both # the newly enabled button as well as the previously enabled (now # disabled) button. enabled = button.get_active() relatedBox.set_sensitive(enabled) self._setup_no_updates() def on_back_clicked(self, button): """If the user entered duplicate repo names, keep them on the screen. Otherwise, do the usual thing.""" ui_repo_names = [r[REPO_OBJ].name for r in self._repoStore] if len(ui_repo_names) != len(frozenset(ui_repo_names)): return else: NormalSpoke.on_back_clicked(self, button) def on_chooser_clicked(self, button): dialog = IsoChooser(self.data) # If the chooser has been run once before, we should make it default to # the previously selected file. if self._currentIsoFile: dialog.refresh(currentFile=self._currentIsoFile) else: dialog.refresh() with enlightbox(self.window, dialog.window): f = dialog.run(self._get_selected_partition()) if f and f.endswith(".iso"): self._currentIsoFile = f button.set_label(os.path.basename(f)) button.set_use_underline(False) self._verifyIsoButton.set_sensitive(True) def on_proxy_clicked(self, button): dialog = ProxyDialog(self.data, self._proxyUrl) with enlightbox(self.window, dialog.window): dialog.refresh() dialog.run() if self._proxyUrl != dialog.proxyUrl: self._proxyChange = True self._proxyUrl = dialog.proxyUrl def on_verify_iso_clicked(self, button): p = self._get_selected_partition() f = self._currentIsoFile if not p or not f: return dialog = MediaCheckDialog(self.data) with enlightbox(self.window, dialog.window): unmount = not p.format.status mounts = get_mount_paths(p.path) # We have to check both ISO_DIR and the DRACUT_ISODIR because we # still reference both, even though /mnt/install is a symlink to # /run/install. Finding mount points doesn't handle the symlink if constants.ISO_DIR not in mounts and constants.DRACUT_ISODIR not in mounts: # We're not mounted to either location, so do the mount p.format.mount(mountpoint=constants.ISO_DIR) dialog.run(constants.ISO_DIR + "/" + f) if unmount: p.format.unmount() def on_verify_media_clicked(self, button): if not self._cdrom: return dialog = MediaCheckDialog(self.data) with enlightbox(self.window, dialog.window): dialog.run("/dev/" + self._cdrom.name) def on_protocol_changed(self, combo): # Only allow the URL entry to be used if we're using an HTTP/FTP # method that's not the mirror list, or an NFS method. self._urlEntry.set_sensitive(self._http_active() or self._ftp_active() or self._nfs_active()) # Only allow thse widgets to be shown if it makes sense for the # the currently selected protocol. self._proxyButton.set_sensitive(self._http_active() or self._mirror_active()) self._nfsOptsBox.set_visible(self._nfs_active()) self._mirrorlistCheckbox.set_visible(self._http_active()) self._setup_no_updates() def _update_payload_repos(self): """ Change the packaging repos to match the new edits This will add new repos to the addon repo list, remove ones that were removed and update any changes made to existing ones. :returns: True if any repo was changed, added or removed :rtype: bool """ REPO_ATTRS = ("name", "baseurl", "mirrorlist", "proxy", "enabled") changed = False ui_orig_names = [r[REPO_OBJ].orig_name for r in self._repoStore] # Remove repos from payload that were removed in the UI for repo_name in [ r for r in self.payload.addOns if r not in ui_orig_names ]: repo = self.payload.getAddOnRepo(repo_name) # TODO: Need an API to do this w/o touching yum (not addRepo) self.payload.data.repo.dataList().remove(repo) changed = True for repo, orig_repo in [ (r[REPO_OBJ], self.payload.getAddOnRepo(r[REPO_OBJ].orig_name)) for r in self._repoStore ]: if not orig_repo: # TODO: Need an API to do this w/o touching yum (not addRepo) self.payload.data.repo.dataList().append(repo) changed = True elif not cmp_obj_attrs(orig_repo, repo, REPO_ATTRS): for attr in REPO_ATTRS: setattr(orig_repo, attr, getattr(repo, attr)) changed = True return changed def _reset_repoStore(self): """ Reset the list of repos. Populate the list with all the addon repos from payload.addOns. If the list has no element, clear the repo entry fields. """ self._repoStore.clear() repos = self.payload.addOns log.debug("Setting up repos: %s", repos) for name in repos: repo = self.payload.getAddOnRepo(name) ks_repo = self.data.RepoData(name=repo.name, baseurl=repo.baseurl, mirrorlist=repo.mirrorlist, proxy=repo.proxy, enabled=repo.enabled) # Track the original name, user may change .name ks_repo.orig_name = name self._repoStore.append( [self.payload.isRepoEnabled(name), ks_repo.name, ks_repo]) if len(self._repoStore) > 0: self._repoSelection.select_path(0) else: self._clear_repo_info() self._repoEntryBox.set_sensitive(False) def on_repoSelection_changed(self, *args): """ Called when the selection changed. Update the repo text boxes with the current information """ itr = self._repoSelection.get_selected()[1] if not itr: return self._update_repo_info(self._repoStore[itr][REPO_OBJ]) def on_repoEnable_toggled(self, renderer, path): """ Called when the repo Enable checkbox is clicked """ enabled = not self._repoStore[path][REPO_ENABLED_COL] self._repoStore[path][REPO_ENABLED_COL] = enabled self._repoStore[path][REPO_OBJ].enabled = enabled def _clear_repo_info(self): """ Clear the text from the repo entry fields and reset the checkbox and combobox. """ self._repoNameEntry.set_text("") self._repoMirrorlistCheckbox.handler_block_by_func( self.on_repoMirrorlistCheckbox_toggled) self._repoMirrorlistCheckbox.set_active(False) self._repoMirrorlistCheckbox.handler_unblock_by_func( self.on_repoMirrorlistCheckbox_toggled) self._repoUrlEntry.set_text("") self._repoProtocolComboBox.set_active(0) self._repoProxyUrlEntry.set_text("") self._repoProxyUsernameEntry.set_text("") self._repoProxyPasswordEntry.set_text("") def _update_repo_info(self, repo): """ Update the text boxes with data from repo :param repo: kickstart repository object :type repo: RepoData """ self._repoNameEntry.set_text(repo.name) self._display_repo_name_message(repo, repo.name) self._repoMirrorlistCheckbox.handler_block_by_func( self.on_repoMirrorlistCheckbox_toggled) if repo.mirrorlist: url = repo.mirrorlist self._repoMirrorlistCheckbox.set_active(True) else: url = repo.baseurl self._repoMirrorlistCheckbox.set_active(False) self._repoMirrorlistCheckbox.handler_unblock_by_func( self.on_repoMirrorlistCheckbox_toggled) if url: for idx, proto in REPO_PROTO: if url.startswith(proto): self._repoProtocolComboBox.set_active(idx) self._repoUrlEntry.set_text(url[len(proto):]) break else: # Unknown protocol, just set the url then self._repoUrlEntry.set_text(url) else: self._repoUrlEntry.set_text("") if not repo.proxy: self._repoProxyUrlEntry.set_text("") self._repoProxyUsernameEntry.set_text("") self._repoProxyPasswordEntry.set_text("") else: try: proxy = ProxyString(repo.proxy) if proxy.username: self._repoProxyUsernameEntry.set_text(proxy.username) if proxy.password: self._repoProxyPasswordEntry.set_text(proxy.password) self._repoProxyUrlEntry.set_text(proxy.noauth_url) except ProxyStringError as e: log.error("Failed to parse proxy for repo %s: %s", repo.name, e) return def _verify_repo_names(self): """ Returns an appropriate error message if the list of repo names contains duplicates. """ repo_names = [r[REPO_OBJ].name for r in self._repoStore] if len(repo_names) != len(frozenset(repo_names)): return N_("Duplicate repository names.") return None def _display_repo_names_message(self): """ Displays a warning if the list of repo names is not valid. Returns the warning message displayed, if any. """ warning_msg = self._verify_repo_names() if warning_msg: self._repoNamesWarningLabel.set_text(_(warning_msg)) really_show(self._repoNamesWarningBox) self.set_warning( _("Duplicate repository names not allowed; choose a unique name for each repository." )) self.window.show_all() else: self._repoNamesWarningLabel.set_text("") really_hide(self._repoNamesWarningBox) self.clear_info() return warning_msg def _verify_repo_name(self, repo, name): """ Returns an appropriate error message if the given name is not valid for this repo. Performs these checks: *) Checks if the string is empty *) Checks if the format is accepted by yum. *) Checks if the repository name coincides with any of the non-additional repositories. :param repo: kickstart repository object :type repo: RepoData :param name: the designated name for the repo :type name: string """ if name == "": return N_("Empty repository name.") allowed_chars = string.ascii_letters + string.digits + '-_.:' if [c for c in name if c not in allowed_chars]: return N_("Invalid repository name.") cnames = [constants.BASE_REPO_NAME] + \ self.payload.DEFAULT_REPOS + \ [r for r in self.payload.repos if r not in self.payload.addOns] if name in cnames: return N_( "Repository name conflicts with internal repository name.") return None def _display_repo_name_message(self, repo, name): """ Displays a warning if the repo name is not valid. Returns the warning message displayed, if any. :param repo: kickstart repository object :type repo: RepoData :param name: the designated name for the repo :type name: string """ warning_msg = self._verify_repo_name(repo, name) if warning_msg: self._repoNameWarningLabel.set_text(_(warning_msg)) really_show(self._repoNameWarningBox) else: self._repoNameWarningLabel.set_text("") really_hide(self._repoNameWarningBox) return warning_msg def on_noUpdatesCheckbox_toggled(self, *args): """ Toggle the enable state of the updates repo Before final release this will also toggle the updates-testing repo """ if self._noUpdatesCheckbox.get_active(): self.payload.disableRepo("updates") if not constants.isFinal: self.payload.disableRepo("updates-testing") else: self.payload.enableRepo("updates") if not constants.isFinal: self.payload.enableRepo("updates-testing") def on_addRepo_clicked(self, button): """ Add a new repository """ repo = self.data.RepoData(name="New_Repository") repo.ks_repo = True repo.orig_name = "" itr = self._repoStore.append([True, repo.name, repo]) self._repoSelection.select_iter(itr) self._repoEntryBox.set_sensitive(True) self._display_repo_name_message(repo, repo.name) self._display_repo_names_message() def on_removeRepo_clicked(self, button): """ Remove the selected repository """ itr = self._repoSelection.get_selected()[1] if not itr: return self._repoStore.remove(itr) if len(self._repoStore) == 0: self._clear_repo_info() self._repoEntryBox.set_sensitive(False) self._display_repo_names_message() def on_resetRepos_clicked(self, button): """ Revert to the default list of repositories """ self._reset_repoStore() def on_repoNameEntry_changed(self, entry): """ repo name changed """ itr = self._repoSelection.get_selected()[1] if not itr: return repo = self._repoStore[itr][REPO_OBJ] name = self._repoNameEntry.get_text().strip() if not self._display_repo_name_message(repo, name): self._repoStore.set_value(itr, REPO_NAME_COL, name) repo.name = name self._display_repo_names_message() def on_repoUrl_changed(self, *args): """ proxy url or protocol changed """ itr = self._repoSelection.get_selected()[1] if not itr: return repo = self._repoStore[itr][REPO_OBJ] idx = self._repoProtocolComboBox.get_active() proto = REPO_PROTO[idx][1] url = self._repoUrlEntry.get_text().strip() if self._repoMirrorlistCheckbox.get_active(): repo.mirorlist = proto + url else: repo.baseurl = proto + url def on_repoMirrorlistCheckbox_toggled(self, *args): """ mirror state changed """ itr = self._repoSelection.get_selected()[1] if not itr: return repo = self._repoStore[itr][REPO_OBJ] # This is called by set_active so only swap if there is something # in the variable. if self._repoMirrorlistCheckbox.get_active() and repo.baseurl: repo.mirrorlist = repo.baseurl repo.baseurl = "" elif repo.mirrorlist: repo.baseurl = repo.mirrorlist repo.mirrorlist = "" def on_repoProxy_changed(self, *args): """ Update the selected repo's proxy settings """ itr = self._repoSelection.get_selected()[1] if not itr: return repo = self._repoStore[itr][REPO_OBJ] url = self._repoProxyUrlEntry.get_text().strip() username = self._repoProxyUsernameEntry.get_text().strip() or None password = self._repoProxyPasswordEntry.get_text().strip() or None try: proxy = ProxyString(url=url, username=username, password=password) repo.proxy = proxy.url except ProxyStringError as e: log.error("Failed to parse proxy - %s:%s@%s: %s", username, password, url, e)
class FilterSpoke(NormalSpoke): """ .. inheritance-diagram:: FilterSpoke :parts: 3 """ builderObjects = ["diskStore", "filterWindow", "searchModel", "multipathModel", "otherModel", "zModel"] mainWidgetName = "filterWindow" uiFile = "spokes/filter.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): onlyuse = self.selected_disks[:] for disk in [d for d in self.storage.disks if d.name in onlyuse]: onlyuse.extend([d.name for d in disk.ancestors if d.name not in onlyuse]) self.data.ignoredisk.onlyuse = onlyuse self.data.clearpart.drives = 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.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() self._store = self.builder.get_object("diskStore") self._addDisksButton = self.builder.get_object("addDisksButton") 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() rc = dialog.run() if rc == 1: self.skipTo = "StorageSpoke" self.on_back_clicked(rc) # 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 UserSpoke(NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ builderObjects = ["userCreationWindow"] mainWidgetName = "userCreationWindow" focusWidgetName = "t_username" 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 == ANACONDA_ENVIRON: return True elif environment == 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 == 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._oldweak = None def initialize(self): NormalSpoke.initialize(self) if self.data.user.userList: self._user = self.data.user.userList[0] else: self._user = self.data.UserData() self._wheel = self.data.GroupData(name="wheel") self._qubes = self.data.GroupData(name="qubes") self._groupDict = {"wheel": self._wheel, "qubes": self._qubes} # placeholders for the text boxes self.username = self.builder.get_object("t_username") self.pw = self.builder.get_object("t_password") self.confirm = self.builder.get_object("t_verifypassword") self.usepassword = self.builder.get_object("c_usepassword") # Counters for checks that ask the user to click Done to confirm self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self.guesser = {self.username: True} # Updated during the password changed event and used by the password # field validity checker self._pwq_error = None self._pwq_valid = True self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("user") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() # indicate when the password was set by kickstart self._user.password_kickstarted = self.data.user.seen # Password checks, in order of importance: # - if a password is required, is one specified? # - if a password is specified and there is data in the confirm box, do they match? # - if a password is specified and the confirm box is empty or match, how strong is it? # - if a strong password is specified, does it contain non-ASCII data? # - if a password is required, is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # the password confirmation needs to be checked whenever either of the password # fields change. attach to the confirm field so that errors focus on confirm, # and check changes to the password field in password_changed self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) # Keep a reference to these checks, since they have to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) self.add_check(self.confirm, self._checkPasswordEmpty) # Allow empty usernames so the spoke can be exited without creating a user self.add_re_check(self.username, re.compile(USERNAME_VALID.pattern + r'|^$'), _("Invalid user name")) # 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. if self._user.password_kickstarted: self.usepassword.set_active(self._user.password != "") if not self._user.isCrypted: self.pw.set_text(self._user.password) self.confirm.set_text(self._user.password) else: self.usepassword.set_active(True) self.pw.set_placeholder_text( _("The password was set by kickstart.")) self.confirm.set_placeholder_text( _("The password was set by kickstart.")) elif not self.policy.emptyok: # Policy is that a non-empty password is required self.usepassword.set_active(True) if not self.policy.emptyok: # User isn't allowed to change whether password is required or not self.usepassword.set_sensitive(False) def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.username.set_text(self._user.name) self.pw.emit("changed") self.confirm.emit("changed") if self.username.get_text() and self.usepassword.get_active() and \ self._user.password == "": self.pw.grab_focus() else: self.username.grab_focus() @property def status(self): if len(self.data.user.userList) == 0: return _("No user will be created") elif self._wheel.name 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): return True 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.usepassword.get_active(): if self.pw.get_text(): self._user.password_kickstarted = False self._user.password = cryptPassword(self.pw.get_text()) self._user.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") # reset the password when the user unselects it else: self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") self._user.password = "" self._user.isCrypted = False self._user.password_kickstarted = False self._user.name = self.username.get_text() # Remove any groups that were created in a previous visit to this spoke self.data.group.groupList = [g for g in self.data.group.groupList \ if not hasattr(g, 'anaconda_group')] # the user will be created only if the username is set if self._user.name: if self._wheel.name not in self._user.groups: self._user.groups.append(self._wheel.name) if self._qubes.name not in self._user.groups: self._user.groups.append(self._qubes.name) anaconda_groups = [ self._groupDict[g] for g in self._user.groups if g not in (self._wheel.name, self._qubes.name) ] self.data.group.groupList += anaconda_groups # Flag the groups as being created in this spoke for g in anaconda_groups: g.anaconda_group = True if self._user not in self.data.user.userList: self.data.user.userList.append(self._user) elif self._user in self.data.user.userList: self.data.user.userList.remove(self._user) @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.policy.changesok) @property def completed(self): return len(self.data.user.userList) > 0 def _updatePwQuality(self): """This method updates the password indicators according to the password entered by the user. """ pwtext = self.pw.get_text() username = self.username.get_text() # Reset the counters used for the "press Done twice" logic self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self._pwq_valid, strength, self._pwq_error = validatePassword( pwtext, username) if not pwtext: val = 0 elif strength < 50: val = 1 elif strength < 75: val = 2 elif strength < 90: val = 3 else: val = 4 text = _(PASSWORD_STRENGTH_DESC[val]) self.pw_bar.set_value(val) self.pw_label.set_text(text) def usepassword_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.""" self.pw.set_sensitive(self.usepassword.get_active()) self.confirm.set_sensitive(self.usepassword.get_active()) # Re-check the password self.pw.emit("changed") self.confirm.emit("changed") def password_changed(self, editable=None, data=None): """Update the password strength level bar""" self._updatePwQuality() # Update the password/confirm match check on changes to the main password field self._confirm_check.update_check_status() def username_changed(self, editable=None, data=None): """Called by Gtk callback when the username or hostname entry changes. It disables the guess algorithm if the user added his own text there and reenable it when the user deletes the whole text.""" if editable.get_text() == "": self.guesser[editable] = True else: self.guesser[editable] = False # Re-run the password checks against the new username self.pw.emit("changed") self.confirm.emit("changed") def _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all. This check is used for both the password and the confirmation. """ # If the password was set by kickstart, skip the strength check if self._user.password_kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK # Skip the check if no password is required if (not self.usepassword.get_active() ) or self._user.password_kickstarted: return InputCheck.CHECK_OK elif not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """If the user has entered confirmation data, check whether it matches the password.""" # Skip the check if no password is required if (not self.usepassword.get_active() ) or self._user.password_kickstarted: result = InputCheck.CHECK_OK elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK return result def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. The password strength has already been checked in _updatePwQuality, called previously in the signal chain. This method converts the data set from there into an error message. The password strength check can be waived by pressing "Done" twice. This is controlled through the self._waiveStrengthClicks counter. The counter is set in on_back_clicked, which also re-runs this check manually. """ # Skip the check if no password is required if (not self.usepassword.get_active()) or \ ((not self.pw.get_text()) and (self._user.password_kickstarted)): return InputCheck.CHECK_OK # If the password failed the validity check, fail this check if (not self._pwq_valid) and (self._pwq_error): return self._pwq_error # use strength from policy, not bars pw = self.pw.get_text() username = self.username.get_text() _valid, pwstrength, _error = validatePassword( pw, username, minlen=self.policy.minlen) if pwstrength < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK elif self._waiveStrengthClicks == 1: if self._pwq_error: return _( PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error else: return _(PASSWORD_WEAK_CONFIRM) else: # non-strict allows done to be clicked twice if self.policy.strict: done_msg = "" else: done_msg = _(PASSWORD_DONE_TWICE) if self._pwq_error: return _(PASSWORD_WEAK_WITH_ERROR ) % self._pwq_error + " " + done_msg else: return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK def _checkPasswordASCII(self, inputcheck): """Set an error message if the password contains non-ASCII characters. Like the password strength check, this check can be bypassed by pressing Done twice. """ # If Done has been clicked, waive the check if self._waiveASCIIClicks > 0: return InputCheck.CHECK_OK password = self.get_input(inputcheck.input_obj) if password and any(char not in PW_ASCII_CHARS for char in password): return _(PASSWORD_ASCII) return InputCheck.CHECK_OK def on_back_clicked(self, button): # If the failed check is for non-ASCII characters, # add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: self._waiveASCIIClicks += 1 self._pwASCIICheck.update_check_status() # If there is no user set, skip the checks if not self.username.get_text(): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
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_id = None self._start_updating_timer_id = None self._shown = False self._tz = None def initialize(self): NormalSpoke.initialize(self) 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") # 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 flags.can_touch_runtime_system("modify system time and date"): self._set_date_time_setting_sensitive(False) self._config_dialog = NTPconfigDialog(self.data) 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_id = None if is_valid_timezone(self.data.timezone.timezone): self._set_timezone(self.data.timezone.timezone) elif not flags.flags.automatedInstall: log.warning( "%s is not a valid timezone, falling back to default (%s)", self.data.timezone.timezone, DEFAULT_TZ) self._set_timezone(DEFAULT_TZ) self.data.timezone.timezone = 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) @property def status(self): if self.data.timezone.timezone: if is_valid_timezone(self.data.timezone.timezone): return _("%s timezone") % get_xlated_timezone( self.data.timezone.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.data.timezone.timezone new_tz = region + "/" + city self.data.timezone.timezone = new_tz if old_tz != new_tz: # new values, not from kickstart self.data.timezone.seen = False self._kickstarted = False self.data.timezone.nontp = not self._ntpSwitch.get_active() def execute(self): if self._update_datetime_timer_id is not None: GLib.source_remove(self._update_datetime_timer_id) self._update_datetime_timer_id = None @property def ready(self): return not threadMgr.get("AnaDateTimeThread") @property def completed(self): if self._kickstarted and not self.data.timezone.seen: # taking values from kickstart, but not specified return False else: return is_valid_timezone(self.data.timezone.timezone) @property def mandatory(self): return True def refresh(self): self._shown = True #update the displayed time self._update_datetime_timer_id = GLib.timeout_add_seconds( 1, self._update_datetime) self._start_updating_timer_id = None if is_valid_timezone(self.data.timezone.timezone): self._tzmap.set_timezone(self.data.timezone.timezone) self._update_datetime() has_active_network = nm.nm_is_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 flags.can_touch_runtime_system("get NTP service state"): ntp_working = has_active_network and iutil.service_running( NTP_SERVICE) else: ntp_working = not self.data.timezone.nontp self._ntpSwitch.set_active(ntp_working) @gtk_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 @gtk_action_nowait def add_to_store_xlated(self, store, item, xlated): store.append([item, xlated]) @gtk_action_nowait def add_to_store(self, store, item): store.append([item]) @gtk_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_id = None if not flags.can_touch_runtime_system("save system time"): 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_id: self._update_datetime_timer_id = GLib.timeout_add_seconds( 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_id: GLib.source_remove(self._update_datetime_timer_id) self._update_datetime_timer_id = None #stop previous $interval seconds timer (see below) if self._start_updating_timer_id: GLib.source_remove(self._start_updating_timer_id) #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_id = GLib.timeout_add_seconds( 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) else: # we don't want the timezone-changed signal to be emitted self._tzmap.set_timezone(timezone) # 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() 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 flags.can_touch_runtime_system("start NTP service"): #cannot touch runtime system, not much to do here return if not nm.nm_is_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 = iutil.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 iutil.service_running(NTP_SERVICE): switch.set_active(False) else: #turned OFF if not flags.can_touch_runtime_system("stop NTP service"): #cannot touch runtime system, nothing to do here return self._set_date_time_setting_sensitive(True) ret = iutil.stop_service(NTP_SERVICE) #if stopping chronyd failed and chronyd is running, #set switch back to ON if (ret != 0) and iutil.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.data.timezone.ntpservers = 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 UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ builderObjects = ["userCreationWindow"] mainWidgetName = "userCreationWindow" focusWidgetName = "t_fullname" 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 == ANACONDA_ENVIRON: return True elif environment == 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 == 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) def initialize(self): NormalSpoke.initialize(self) # 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() # placeholders for the text boxes self.fullname = self.builder.get_object("t_fullname") self.username = self.builder.get_object("t_username") self.pw = self.builder.get_object("t_password") self.confirm = self.builder.get_object("t_verifypassword") self.admin = self.builder.get_object("c_admin") self.usepassword = self.builder.get_object("c_usepassword") self.b_advanced = self.builder.get_object("b_advanced") # Counters for checks that ask the user to click Done to confirm self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 self.guesser = True self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) self.pw_bar.add_offset_value("full", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("user") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() # indicate when the password was set by kickstart self._password_kickstarted = self.data.user.seen # Password checks, in order of importance: # - if a password is required, is one specified? # - if a password is specified and there is data in the confirm box, do they match? # - if a password is specified and the confirm box is empty or match, how strong is it? # - if a strong password is specified, does it contain non-ASCII data? # - if a password is required, is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # the password confirmation needs to be checked whenever either of the password # fields change. attach to the confirm field so that errors focus on confirm, # and check changes to the password field in password_changed self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) # Keep a reference to these checks, since they have to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) self.add_check(self.confirm, self._checkPasswordEmpty) self.add_check(self.username, self._checkUsername) self.add_re_check(self.fullname, GECOS_VALID, _("Full name cannot contain colon characters")) # 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. if self._password_kickstarted: self.usepassword.set_active(True) self.pw.set_placeholder_text( _("The password was set by kickstart.")) self.confirm.set_placeholder_text( _("The password was set by kickstart.")) elif not self.policy.emptyok: # Policy is that a non-empty password is required self.usepassword.set_active(True) if not self.policy.emptyok: # User isn't allowed to change whether password is required or not self.usepassword.set_sensitive(False) self._advanced = AdvancedUserDialog(self._user, self.data) self._advanced.initialize() # set the visibility of the password entries set_password_visibility(self.pw, False) set_password_visibility(self.confirm, False) def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.username.set_text(self._user.name) self.fullname.set_text(self._user.gecos) self.admin.set_active("wheel" in self._user.groups) self.pw.emit("changed") self.confirm.emit("changed") @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.data.rootpw.password and not self.data.rootpw.lock 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.usepassword.get_active(): if self.pw.get_text(): self._password_kickstarted = False self._user.password = cryptPassword(self.pw.get_text()) self._user.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") # reset the password when the user unselects it else: self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") self._user.password = "" self._user.isCrypted = False self._password_kickstarted = False self._user.name = self.username.get_text() self._user.gecos = self.fullname.get_text() # 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.policy.changesok) @property def completed(self): return len(self.data.user.userList) > 0 def _updatePwQuality(self, empty, strength): """This method updates the password indicators according to the password entered by the user. """ # If the password is empty, clear the strength bar if empty: val = 0 elif strength < 50: val = 1 elif strength < 75: val = 2 elif strength < 90: val = 3 else: val = 4 text = _(PASSWORD_STRENGTH_DESC[val]) self.pw_bar.set_value(val) self.pw_label.set_text(text) def usepassword_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.""" self.pw.set_sensitive(togglebutton.get_active()) self.confirm.set_sensitive(togglebutton.get_active()) # Re-check the password self.pw.emit("changed") self.confirm.emit("changed") def password_changed(self, editable=None, data=None): """Update the password strength level bar""" # Reset the counters used for the "press Done twice" logic self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 # Update the password/confirm match check on changes to the main password field self._confirm_check.update_check_status() 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 username_changed(self, editable, data=None): """Called by Gtk on all username changes.""" # Disable the advanced user dialog button when no username is set if editable.get_text(): self.b_advanced.set_sensitive(True) else: self.b_advanced.set_sensitive(False) # Re-run the password checks against the new username self.pw.emit("changed") self.confirm.emit("changed") def full_name_changed(self, editable, data=None): """Called by Gtk callback when the full name field changes.""" if self.guesser: fullname = editable.get_text() username = guess_username(fullname) with blockedHandler(self.username, self.on_username_set_by_user): self.username.set_text(username) 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 _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all. This check is used for both the password and the confirmation. """ # If the password was set by kickstart, skip the strength check if self._password_kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK # Skip the check if no password is required if (not self.usepassword.get_active()) or self._password_kickstarted: return InputCheck.CHECK_OK elif not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """If the user has entered confirmation data, check whether it matches the password.""" # Skip the check if no password is required if (not self.usepassword.get_active()) or self._password_kickstarted: result = InputCheck.CHECK_OK elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK return result def _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. The password strength check can be waived by pressing "Done" twice. This is controlled through the self._waiveStrengthClicks counter. The counter is set in on_back_clicked, which also re-runs this check manually. """ # Skip the check if no password is required if not self.usepassword.get_active or self._password_kickstarted: return InputCheck.CHECK_OK # If the password is empty, clear the strength bar and skip this check pw = self.pw.get_text() if not pw: self._updatePwQuality(True, 0) return InputCheck.CHECK_OK # determine the password strength username = self.username.get_text() valid, pwstrength, error = validatePassword(pw, username, minlen=self.policy.minlen) # set the strength bar self._updatePwQuality(False, pwstrength) # If the password failed the validity check, fail this check if not valid and error: return error if pwstrength < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK elif self._waiveStrengthClicks == 1: if error: return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % error else: return _(PASSWORD_WEAK_CONFIRM) else: # non-strict allows done to be clicked twice if self.policy.strict: done_msg = "" else: done_msg = _(PASSWORD_DONE_TWICE) if error: return _(PASSWORD_WEAK_WITH_ERROR) % error + " " + done_msg else: return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK def _checkPasswordASCII(self, inputcheck): """Set an error message if the password contains non-ASCII characters. Like the password strength check, this check can be bypassed by pressing Done twice. """ # If Done has been clicked, waive the check if self._waiveASCIIClicks > 0: return InputCheck.CHECK_OK password = self.get_input(inputcheck.input_obj) if password and any(char not in PW_ASCII_CHARS for char in password): return _(PASSWORD_ASCII) return InputCheck.CHECK_OK def _checkUsername(self, inputcheck): name = self.get_input(inputcheck.input_obj) # Allow empty usernames so the spoke can be exited without creating a user if name == "": return InputCheck.CHECK_OK valid, msg = check_username(name) if valid: return InputCheck.CHECK_OK else: return msg or _("Invalid user name") def on_advanced_clicked(self, _button, data=None): """Handler for the Advanced.. button. It starts the Advanced dialog for setting homedit, uid, gid and groups. """ self._user.name = self.username.get_text() self._advanced.refresh() with self.main_window.enlightbox(self._advanced.window): self._advanced.run() self.admin.set_active("wheel" in self._user.groups) def on_back_clicked(self, button): # If the failed check is for non-ASCII characters, # add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: self._waiveASCIIClicks += 1 self._pwASCIICheck.update_check_status() # If there is no user set, skip the checks if not self.username.get_text(): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
class KeyboardSpoke(NormalSpoke): builderObjects = ["addedLayoutStore", "keyboardWindow", "layoutTestBuffer"] mainWidgetName = "keyboardWindow" uiFile = "spokes/keyboard.glade" helpFile = "KeyboardSpoke.xml" category = LocalizationCategory icon = "input-keyboard-symbolic" title = CN_("GUI|Spoke", "_KEYBOARD") def __init__(self, *args): NormalSpoke.__init__(self, *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") def apply(self): # the user has confirmed (seen) the configuration self._confirmed = True # Clear and repopulate self.data with actual values self.data.keyboard.x_layouts = list() self.data.keyboard.seen = True for row in self._store: self.data.keyboard.x_layouts.append(row[0]) @property def completed(self): if flags.flags.automatedInstall and not self.data.keyboard.seen: return False elif not self._confirmed and \ self._xkl_wrapper.get_current_layout() != self.data.keyboard.x_layouts[0] and \ not flags.flags.usevnc: # the currently activated layout is a different one from the # installed system's default. Ignore VNC, since VNC keymaps are # weird and more on the client side. return False else: 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): NormalSpoke.initialize(self) self._add_dialog = AddLayoutDialog(self.data) self._add_dialog.initialize() if flags.can_touch_runtime_system( "hide runtime keyboard configuration " "warning", touch_live=True): 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._switching_dialog.initialize() self._layoutSwitchLabel = self.builder.get_object("layoutSwitchLabel") if not flags.can_touch_runtime_system("test X layouts", touch_live=True): # 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__, False) def refresh(self): NormalSpoke.refresh(self) # 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 self.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 flags.can_touch_runtime_system("add runtime X layout", touch_live=True): 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 flags.can_touch_runtime_system("remove runtime X layout", touch_live=True): self._xkl_wrapper.remove_layout(store[itr][0]) store.remove(itr) def _refresh_switching_info(self): if self.data.keyboard.switch_options: first_option = self.data.keyboard.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 flags.can_touch_runtime_system("reorder runtime X layouts", touch_live=True): 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 flags.can_touch_runtime_system("reorder runtime X layouts", touch_live=True): 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() 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.data.keyboard.switch_options = new_options # Refresh switching info label. self._refresh_switching_info() def _add_data_layouts(self): if not self.data.keyboard.x_layouts: # nothing specified, just add the default self._addLayout(self._store, DEFAULT_KEYBOARD) return valid_layouts = [] for layout in self.data.keyboard.x_layouts: 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.data.keyboard.x_layouts = [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 FilterSpoke(NormalSpoke): builderObjects = ["diskStore", "filterWindow", "searchModel", "multipathModel", "otherModel", "raidModel", "zModel"] mainWidgetName = "filterWindow" uiFile = "spokes/filter.glade" 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): onlyuse = self.selected_disks[:] for disk in [d for d in self.storage.disks if d.name in onlyuse]: onlyuse.extend([d.name for d in disk.ancestors if d.name not in onlyuse]) self.data.ignoredisk.onlyuse = onlyuse self.data.clearpart.drives = self.selected_disks[:] def initialize(self): NormalSpoke.initialize(self) self.pages = [SearchPage(self.storage, self.builder), MultipathPage(self.storage, self.builder), OtherPage(self.storage, self.builder), RaidPage(self.storage, self.builder), ZPage(self.storage, self.builder)] self._notebook = self.builder.get_object("advancedNotebook") if not arch.isS390(): self._notebook.remove_page(-1) self.builder.get_object("addZFCPButton").destroy() if not has_fcoe(): self.builder.get_object("addFCOEButton").destroy() self._store = self.builder.get_object("diskStore") self._addDisksButton = self.builder.get_object("addDisksButton") 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 = itertools.chain(*map(self._real_ancestors, self.disks)) self.ancestors = map(lambda d: d.name, self.ancestors) self._store.clear() allDisks = [] multipathDisks = [] otherDisks = [] raidDisks = [] 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 itertools.ifilterfalse(isLocalDisk, 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): raidDisks.append(disk) elif self.pages[4].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, raidDisks) self.pages[4].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.getFreeSpace(disks=disks) with enlightbox(self.window, dialog.window): dialog.refresh(disks, free_space, showRemove=False, setBoot=False) dialog.run() def on_find_clicked(self, button): n = self._notebook.get_current_page() self.pages[n].filterActive = True self.pages[n].model.refilter() def on_clear_clicked(self, button): n = self._notebook.get_current_page() self.pages[n].filterActive = False self.pages[n].model.refilter() self.pages[n].clear() 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 itr = self._store.get_iter(path) 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() def on_add_iscsi_clicked(self, widget, *args): dialog = ISCSIDialog(self.data, self.storage) with enlightbox(self.window, 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 enlightbox(self.window, 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): pass ## ## SEARCH TAB SIGNAL HANDLERS ## def on_search_type_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("searchTypeNotebook") findButton = self.builder.get_object("searchFindButton") clearButton = self.builder.get_object("searchClearButton") findButton.set_sensitive(ndx != 0) clearButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx) ## ## MULTIPATH TAB SIGNAL HANDLERS ## def on_multipath_type_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("multipathTypeNotebook") findButton = self.builder.get_object("multipathFindButton") clearButton = self.builder.get_object("multipathClearButton") findButton.set_sensitive(ndx != 0) clearButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx) ## ## OTHER TAB SIGNAL HANDLERS ## def on_other_type_combo_changed(self, combo): ndx = combo.get_active() notebook = self.builder.get_object("otherTypeNotebook") findButton = self.builder.get_object("otherFindButton") clearButton = self.builder.get_object("otherClearButton") findButton.set_sensitive(ndx != 0) clearButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx)
class LangsupportSpoke(LangLocaleHandler, NormalSpoke): builderObjects = [ "languageStore", "languageStoreFilter", "localeStore", "langsupportWindow" ] mainWidgetName = "langsupportWindow" focusWidgetName = "languageEntry" uiFile = "spokes/langsupport.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._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) for col, rend in [("nativeName", "nativeNameRenderer"), ("englishName", "englishNameRenderer")]: column = self.builder.get_object(col) renderer = self.builder.get_object(rend) override_cell_property(column, renderer, "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) 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): return not flags.livecdInstall @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.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])
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ .. inheritance-diagram:: PasswordSpoke :parts: 3 """ builderObjects = ["passwordWindow"] mainWidgetName = "passwordWindow" focusWidgetName = "pw" uiFile = "spokes/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._lock = self.data.rootpw.lock self._kickstarted = False def initialize(self): NormalSpoke.initialize(self) # place holders for the text boxes self.pw = self.builder.get_object("pw") self.confirm = self.builder.get_object("confirmPW") self.lock = self.builder.get_object("lock") # 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? # - Is there any data in the confirm box? self.add_check(self.pw, self._checkPasswordEmpty) # the password confirmation needs to be checked whenever either of the password # fields change. attach to the confirm field so that errors focus on confirm, # and check changes to the password field in on_password_changed self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) # Keep a reference for these checks, since they have to be manually run for the # click Done twice check. self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) self.add_check(self.confirm, self._checkPasswordEmpty) # Counters for checks that ask the user to click Done to confirm self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 # Password validation data self._pwq_error = None self._pwq_valid = True self._kickstarted = self.data.rootpw.seen if self._kickstarted: self.pw.set_placeholder_text(_("The password is set.")) self.confirm.set_placeholder_text(_("The password is set.")) self._lock = self.data.rootpw.lock self.pw_bar = self.builder.get_object("password_bar") self.pw_label = self.builder.get_object("password_label") # Configure levels for the password bar self.pw_bar.add_offset_value("low", 2) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) self.pw_bar.add_offset_value("full", 4) # Configure the password policy, if available. Otherwise use defaults. self.policy = self.data.anaconda.pwpolicy.get_policy("root") if not self.policy: self.policy = self.data.anaconda.PwPolicyData() # set the visibility of the password entries set_password_visibility(self.pw, False) set_password_visibility(self.confirm, False) def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: check.enabled = True self.lock.set_active(self._lock) self.on_lock_clicked(self.lock) self.pw.emit("changed") self.confirm.emit("changed") def on_lock_clicked(self, lock): self.pw.set_sensitive(not lock.get_active()) self.confirm.set_sensitive(not lock.get_active()) if not lock.get_active(): self.pw.grab_focus() self._lock = lock.get_active() # Caps lock detection isn't hooked up right now # def setCapsLockLabel(self): # if isCapsLockEnabled(): # self.capslock.set_text("<b>" + _("Caps Lock is on.") + "</b>") # self.capslock.set_use_markup(True) # else: # self.capslock..set_text("") @property def status(self): if self.data.rootpw.lock: return _("Root account is disabled") elif self.data.rootpw.password: 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.pw.get_text() # value from the kickstart changed self.data.rootpw.seen = False self._kickstarted = False if self._lock: self.data.rootpw.lock = True elif pw: self.data.rootpw.lock = False self.data.rootpw.password = cryptPassword(pw) self.data.rootpw.isCrypted = True self.pw.set_placeholder_text("") self.confirm.set_placeholder_text("") @property def completed(self): return bool(self.data.rootpw.password or self.data.rootpw.lock) @property def sensitive(self): return not (self.completed and flags.automatedInstall and self.data.rootpw.seen) def _checkPasswordEmpty(self, inputcheck): """Check whether a password has been specified at all.""" # If the password was set by kickstart, skip this check if self._kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK if self.lock.get_active(): return InputCheck.CHECK_OK if not self.get_input(inputcheck.input_obj): if inputcheck.input_obj == self.pw: return _(PASSWORD_EMPTY_ERROR) else: return _(PASSWORD_CONFIRM_ERROR_GUI) else: return InputCheck.CHECK_OK def _checkPasswordConfirm(self, inputcheck): """Check whether the password matches the confirmation data.""" pw = self.pw.get_text() confirm = self.confirm.get_text() lock = self.lock.get_active() if lock: self._lock = True self._password = None self.clear_info() self._error = False result = InputCheck.CHECK_OK # Skip the check if no password is required elif (not pw and not confirm) and self._kickstarted: result = InputCheck.CHECK_OK elif confirm and (pw != confirm): result = _(PASSWORD_CONFIRM_ERROR_GUI) else: result = InputCheck.CHECK_OK return result def _updatePwQuality(self, empty, strength): """Update the password quality information. """ # If the password is empty, clear the strength bar if empty: val = 0 elif strength < 50: val = 1 elif strength < 75: val = 2 elif strength < 90: val = 3 else: val = 4 text = _(PASSWORD_STRENGTH_DESC[val]) self.pw_bar.set_value(val) self.pw_label.set_text(text) def on_password_changed(self, editable, data=None): # Reset the counters used for the "press Done twice" logic self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 # Update the password/confirm match check on changes to the main password field self._confirm_check.update_check_status() 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 _checkPasswordStrength(self, inputcheck): """Update the error message based on password strength. Update the password strength bar and set an error message. """ pw = self.pw.get_text() confirm = self.confirm.get_text() # Skip the check if no password is required if self._kickstarted: return InputCheck.CHECK_OK # If the password is empty, clear the strength bar and skip this check if self.lock.get_active() or (not pw and not confirm): self._updatePwQuality(True, 0) return InputCheck.CHECK_OK # determine the password strength valid, pwstrength, error = validatePassword(pw, "root", minlen=self.policy.minlen) # set the strength bar self._updatePwQuality(False, pwstrength) # If the password failed the validity check, fail this check if not valid and error: return error if pwstrength < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK elif self._waiveStrengthClicks == 1: if error: return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % error else: return _(PASSWORD_WEAK_CONFIRM) else: # non-strict allows done to be clicked twice if self.policy.strict: done_msg = "" else: done_msg = _(PASSWORD_DONE_TWICE) if error: return _(PASSWORD_WEAK_WITH_ERROR) % error + " " + done_msg else: return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK def _checkPasswordASCII(self, inputcheck): """Set an error message if the password contains non-ASCII characters. Like the password strength check, this check can be bypassed by pressing Done twice. """ # If Done has been clicked, waive the check if self._waiveASCIIClicks > 0: return InputCheck.CHECK_OK password = self.get_input(inputcheck.input_obj) if password and any(char not in PW_ASCII_CHARS for char in password): return _(PASSWORD_ASCII) return InputCheck.CHECK_OK def on_back_clicked(self, button): # If the failed check is for password strength or non-ASCII # characters, add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: self._waiveASCIIClicks += 1 self._pwASCIICheck.update_check_status() # If neither the password nor the confirm field are set, skip the checks if (not self.pw.get_text()) and (not self.confirm.get_text()): for check in self.checks: check.enabled = False if GUISpokeInputCheckHandler.on_back_clicked(self, button): NormalSpoke.on_back_clicked(self, button)
def get_container_type(device_type): return CONTAINER_TYPES.get( device_type, ContainerType( N_("container"), CN_("GUI|Custom Partitioning|Configure|Devices", "container")))
class LangsupportSpoke(LangLocaleHandler, NormalSpoke): builderObjects = [ "languageStore", "languageStoreFilter", "localeStore", "langsupportWindow" ] mainWidgetName = "langsupportWindow" uiFile = "spokes/langsupport.glade" 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._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") localeNativeColumn.set_cell_data_func(localeNativeNameRenderer, self._mark_selected_locale_bold) for col, rend in [("nativeName", "nativeNameRenderer"), ("englishName", "englishNameRenderer")]: column = self.builder.get_object(col) renderer = self.builder.get_object(rend) column.set_cell_data_func(renderer, self._mark_selected_language_bold) 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): return not flags.livecdInstall @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.data.lang.lang ]) def _mark_selected_locale_bold(self, column, renderer, model, itr, user_data=None): if model[itr][2]: renderer.set_property("weight", Pango.Weight.BOLD.real) else: renderer.set_property("weight", Pango.Weight.NORMAL.real) def _mark_selected_language_bold(self, column, renderer, model, itr, user_data=None): lang_locales = set(localization.get_language_locales(model[itr][2])) if not lang_locales.isdisjoint(self._selected_locales): renderer.set_property("weight", Pango.Weight.BOLD.real) else: renderer.set_property("weight", Pango.Weight.NORMAL.real) # 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])