예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
파일: storage.py 프로젝트: numbnet/anaconda
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()
예제 #4
0
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()
예제 #5
0
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)
예제 #6
0
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.
예제 #7
0
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
예제 #8
0
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)
예제 #9
0
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
예제 #10
0
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)
예제 #11
0
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.")
예제 #12
0
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.")
예제 #13
0
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)
예제 #14
0
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()
예제 #15
0
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)
예제 #16
0
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()
예제 #17
0
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)
예제 #18
0
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)
예제 #19
0
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)
예제 #20
0
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])
예제 #21
0
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)
예제 #22
0
def get_container_type(device_type):
    return CONTAINER_TYPES.get(
        device_type,
        ContainerType(
            N_("container"),
            CN_("GUI|Custom Partitioning|Configure|Devices", "container")))
예제 #23
0
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])