Пример #1
0
    def refresh(self):
        self._sectorSizeCombo.set_active(0)

        if self._namespaces:
            self._devicesLabel.set_text("%s" % ", ".join(self._namespaces))
        else:
            self._sectorSizeCombo.set_sensitive(False)
            self._okButton.set_sensitive(False)
            self._startButton.set_sensitive(False)
            self._sectorSizeLabel.set_sensitive(False)
            self._infoLabel.set_text(
                CN_("GUI|Advanced Storage|NVDIM",
                    "No device to be reconfigured selected."))
Пример #2
0
 def refresh(self):
     self._sectorSizeSpinButton.set_value(DEFAULT_SECTOR_SIZE)
     if self.namespaces:
         self._devicesLabel.set_text("%s" % ", ".join(self.namespaces))
     else:
         msg = CN_("GUI|Advanced Storage|NVDIM",
                   "No device to be reconfigured selected.")
         self._infoLabel.set_text(msg)
         for widget in [
                 self._sectorSizeSpinButton, self._okButton,
                 self._startButton, self._sectorSizeLabel
         ]:
             widget.set_sensitive(False)
Пример #3
0
class LangsupportSpoke(NormalSpoke, LangLocaleHandler):
    """
       .. inheritance-diagram:: LangsupportSpoke
          :parts: 3
    """
    builderObjects = [
        "languageStore", "languageStoreFilter", "localeStore",
        "langsupportWindow"
    ]
    mainWidgetName = "langsupportWindow"
    focusWidgetName = "languageEntry"
    uiFile = "spokes/language_support.glade"
    category = LocalizationCategory
    icon = "accessories-character-map-symbolic"
    title = CN_("GUI|Spoke", "_Language Support")

    @staticmethod
    def get_screen_id():
        """Return a unique id of this UI screen."""
        return "language-configuration"

    @classmethod
    def should_run(cls, environment, data):
        """Should the spoke run?"""
        if not is_module_available(LOCALIZATION):
            return False

        if not NormalSpoke.should_run(environment, data):
            return False

        # Don't show the language support spoke on live media.
        return context.payload_type not in PAYLOAD_LIVE_TYPES

    def __init__(self, *args, **kwargs):
        NormalSpoke.__init__(self, *args, **kwargs)
        LangLocaleHandler.__init__(self)
        self._selected_locales = set()

        self._l12_module = LOCALIZATION.get_proxy()

    def initialize(self):
        self.initialize_start()
        self._languageStore = self.builder.get_object("languageStore")
        self._languageEntry = self.builder.get_object("languageEntry")
        self._languageStoreFilter = self.builder.get_object(
            "languageStoreFilter")
        self._langView = self.builder.get_object("languageView")
        self._langSelectedRenderer = self.builder.get_object(
            "langSelectedRenderer")
        self._langSelectedColumn = self.builder.get_object(
            "langSelectedColumn")
        self._langSelection = self.builder.get_object("languageViewSelection")
        self._localeStore = self.builder.get_object("localeStore")
        self._localeView = self.builder.get_object("localeView")

        LangLocaleHandler.initialize(self)

        # mark selected locales and languages with selected locales bold
        localeNativeColumn = self.builder.get_object("localeNativeName")
        localeNativeNameRenderer = self.builder.get_object(
            "localeNativeNameRenderer")
        override_cell_property(localeNativeColumn, localeNativeNameRenderer,
                               "weight", self._mark_selected_locale_bold)

        languageNameColumn = self.builder.get_object("nameColumn")
        nativeNameRenderer = self.builder.get_object("nativeNameRenderer")
        englishNameRenderer = self.builder.get_object("englishNameRenderer")
        override_cell_property(languageNameColumn, nativeNameRenderer,
                               "weight", self._mark_selected_language_bold)
        override_cell_property(languageNameColumn, englishNameRenderer,
                               "weight", self._mark_selected_language_bold)

        # If a language has selected locales, highlight every column so that
        # the row appears highlighted
        for col in self._langView.get_columns():
            for rend in col.get_cells():
                override_cell_property(col, rend, "cell-background-rgba",
                                       self._highlight_selected_language)

        # and also set an icon so that we don't depend on a color to convey information
        highlightedColumn = self.builder.get_object("highlighted")
        highlightedRenderer = self.builder.get_object("highlightedRenderer")
        override_cell_property(highlightedColumn, highlightedRenderer,
                               "icon-name", self._render_lang_highlighted)

        # report that we are done
        self.initialize_done()

    def apply(self):
        # store only additional langsupport locales
        added = sorted(self._selected_locales -
                       set([self._l12_module.Language]))
        self._l12_module.SetLanguageSupport(added)

    def refresh(self):
        self._languageEntry.set_text("")
        self._selected_locales = set(self._installed_langsupports)

        # select the first locale from the "to be installed" langsupports
        self._select_locale(self._installed_langsupports[0])

    @property
    def _installed_langsupports(self):
        return [self._l12_module.Language] + sorted(
            self._l12_module.LanguageSupport)

    @property
    def status(self):
        return ", ".join(
            localization.get_native_name(locale)
            for locale in self._installed_langsupports)

    @property
    def mandatory(self):
        return False

    @property
    def completed(self):
        return True

    def _add_language(self, store, native, english, lang):
        native_span = '<span lang="%s">%s</span>' % \
                (escape_markup(lang), escape_markup(native))
        store.append([native_span, english, lang])

    def _add_locale(self, store, native, locale):
        native_span = '<span lang="%s">%s</span>' % \
                (escape_markup(re.sub(r'\..*', '', locale)),
                 escape_markup(native))

        # native, locale, selected, additional
        store.append([
            native_span, locale, locale in self._selected_locales,
            locale != self._l12_module.Language
        ])

    def _mark_selected_locale_bold(self,
                                   column,
                                   renderer,
                                   model,
                                   itr,
                                   user_data=None):
        if model[itr][2]:
            return Pango.Weight.BOLD.real
        else:
            return Pango.Weight.NORMAL.real

    def _is_lang_selected(self, lang):
        lang_locales = set(localization.get_language_locales(lang))
        return not lang_locales.isdisjoint(self._selected_locales)

    def _mark_selected_language_bold(self,
                                     column,
                                     renderer,
                                     model,
                                     itr,
                                     user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return Pango.Weight.BOLD.real
        else:
            return Pango.Weight.NORMAL.real

    def _highlight_selected_language(self,
                                     column,
                                     renderer,
                                     model,
                                     itr,
                                     user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return _HIGHLIGHT_COLOR
        else:
            return None

    def _render_lang_highlighted(self,
                                 column,
                                 renderer,
                                 model,
                                 itr,
                                 user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return "emblem-ok-symbolic"
        else:
            return None

    # Signal handlers.
    def on_locale_toggled(self, renderer, path):
        itr = self._localeStore.get_iter(path)
        row = self._localeStore[itr]

        row[2] = not row[2]

        if row[2]:
            self._selected_locales.add(row[1])
        else:
            self._selected_locales.remove(row[1])
Пример #4
0
class KeyboardSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: KeyboardSpoke
          :parts: 3
    """
    builderObjects = ["addedLayoutStore", "keyboardWindow", "layoutTestBuffer"]
    mainWidgetName = "keyboardWindow"
    uiFile = "spokes/keyboard.glade"
    category = LocalizationCategory
    icon = "input-keyboard-symbolic"
    title = CN_("GUI|Spoke", "_Keyboard")

    @staticmethod
    def get_screen_id():
        """Return a unique id of this UI screen."""
        return "keyboard-configuration"

    @classmethod
    def should_run(cls, environment, data):
        """Should the spoke run?"""
        if not is_module_available(LOCALIZATION):
            return False

        return NormalSpoke.should_run(environment, data)

    def __init__(self, *args):
        super().__init__(*args)
        self._remove_last_attempt = False
        self._confirmed = False
        self._xkl_wrapper = XklWrapper.get_instance()
        self._add_dialog = None
        self._ready = False

        self._upButton = self.builder.get_object("upButton")
        self._downButton = self.builder.get_object("downButton")
        self._removeButton = self.builder.get_object("removeLayoutButton")
        self._previewButton = self.builder.get_object("previewButton")

        self._l12_module = LOCALIZATION.get_proxy()
        self._seen = self._l12_module.KeyboardKickstarted

    def apply(self):
        # the user has confirmed (seen) the configuration
        self._confirmed = True
        self._seen = True

        # Update module with actual values
        layouts = [row[0] for row in self._store]
        self._l12_module.SetXLayouts(layouts)

    @property
    def completed(self):
        if flags.flags.automatedInstall and not self._seen:
            return False

        # The currently activated layout is a different from the
        # selected ones. Ignore VNC, since VNC keymaps are weird
        # and more on the client side.
        if not self._confirmed and not flags.flags.usevnc \
                and self._xkl_wrapper.get_current_layout() not in self._l12_module.XLayouts:
            return False

        return True

    @property
    def status(self):
        # We don't need to check that self._store is empty, because that isn't allowed.
        descriptions = (self._xkl_wrapper.get_layout_variant_description(
            row[0]) for row in self._store)
        return ", ".join(descriptions)

    @property
    def ready(self):
        return self._ready and threadMgr.get(
            ADD_LAYOUTS_INITIALIZE_THREAD) is None

    def initialize(self):
        super().initialize()
        self.initialize_start()

        # set X keyboard defaults
        # - this needs to be done early in spoke initialization so that
        #   the spoke status does not show outdated keyboard selection
        keyboard.set_x_keyboard_defaults(self._l12_module, self._xkl_wrapper)

        # make sure the x_layouts list has at least one keyboard layout
        if not self._l12_module.XLayouts:
            self._l12_module.SetXLayouts([DEFAULT_KEYBOARD])

        self._add_dialog = AddLayoutDialog(self.data)
        self._add_dialog.initialize()

        if conf.system.can_configure_keyboard:
            self.builder.get_object("warningBox").hide()

        # We want to store layouts' names but show layouts as
        # 'language (description)'.
        layoutColumn = self.builder.get_object("layoutColumn")
        layoutRenderer = self.builder.get_object("layoutRenderer")
        override_cell_property(layoutColumn, layoutRenderer, "text",
                               _show_layout, self._xkl_wrapper)

        self._store = self.builder.get_object("addedLayoutStore")
        self._add_data_layouts()

        self._selection = self.builder.get_object("layoutSelection")

        self._switching_dialog = ConfigureSwitchingDialog(
            self.data, self._l12_module)
        self._switching_dialog.initialize()

        self._layoutSwitchLabel = self.builder.get_object("layoutSwitchLabel")

        if not conf.system.can_configure_keyboard:
            # Disable area for testing layouts as we cannot make
            # it work without modifying runtime system

            widgets = [
                self.builder.get_object("testingLabel"),
                self.builder.get_object("testingWindow"),
                self.builder.get_object("layoutSwitchLabel")
            ]

            # Use testingLabel's text to explain why this part is not
            # sensitive.
            widgets[0].set_text(
                _("Testing layouts configuration not "
                  "available."))

            for widget in widgets:
                widget.set_sensitive(False)

        hubQ.send_not_ready(self.__class__.__name__)
        hubQ.send_message(self.__class__.__name__,
                          _("Getting list of layouts..."))
        threadMgr.add(
            AnacondaThread(name=THREAD_KEYBOARD_INIT, target=self._wait_ready))

    def _wait_ready(self):
        self._add_dialog.wait_initialize()
        self._ready = True
        hubQ.send_ready(self.__class__.__name__)

        # report that the keyboard spoke initialization has been completed
        self.initialize_done()

    def refresh(self):
        super().refresh()

        # Clear out the layout testing box every time the spoke is loaded.  It
        # doesn't make sense to leave temporary data laying around.
        buf = self.builder.get_object("layoutTestBuffer")
        buf.set_text("")

        # Clear and repopulate addedLayoutStore with values from the module data
        self._store.clear()
        self._add_data_layouts()

        # Start with no buttons enabled, since nothing is selected.
        self._upButton.set_sensitive(False)
        self._downButton.set_sensitive(False)
        self._removeButton.set_sensitive(False)
        self._previewButton.set_sensitive(False)

        self._refresh_switching_info()

    def _addLayout(self, store, name):
        # first try to add the layout
        if conf.system.can_configure_keyboard:
            self._xkl_wrapper.add_layout(name)

        # valid layout, append it to the store
        store.append([name])

    def _removeLayout(self, store, itr):
        """
        Remove the layout specified by store iterator from the store and
        X runtime configuration.

        """

        if conf.system.can_configure_keyboard:
            self._xkl_wrapper.remove_layout(store[itr][0])
        store.remove(itr)

    def _refresh_switching_info(self):
        switch_options = self._l12_module.LayoutSwitchOptions
        if flags.flags.usevnc:
            self._layoutSwitchLabel.set_text(
                _("Keyboard layouts are not "
                  "supported when using VNC.\n"
                  "However the settings will be used "
                  "after the installation."))
        elif switch_options:
            first_option = switch_options[0]
            desc = self._xkl_wrapper.get_switch_opt_description(first_option)

            self._layoutSwitchLabel.set_text(_(LAYOUT_SWITCHING_INFO) % desc)
        else:
            self._layoutSwitchLabel.set_text(
                _("Layout switching not "
                  "configured."))

    # Signal handlers.
    def on_add_clicked(self, button):
        self._add_dialog.refresh()

        with self.main_window.enlightbox(self._add_dialog.window):
            response = self._add_dialog.run()

        if response == 1:
            duplicates = set()
            for row in self._store:
                item = row[0]
                if item in self._add_dialog.chosen_layouts:
                    duplicates.add(item)

            for layout in self._add_dialog.chosen_layouts:
                if layout not in duplicates:
                    self._addLayout(self._store, layout)

            if self._remove_last_attempt:
                itr = self._store.get_iter_first()
                if not self._store[itr][0] in self._add_dialog.chosen_layouts:
                    self._removeLayout(self._store, itr)
                self._remove_last_attempt = False

            # Update the selection information
            self._selection.emit("changed")

    def on_remove_clicked(self, button):
        if not self._selection.count_selected_rows():
            return

        (store, itr) = self._selection.get_selected()
        itr2 = store.get_iter_first()
        #if the first item is selected, try to select the next one
        if store[itr][0] == store[itr2][0]:
            itr2 = store.iter_next(itr2)
            if itr2:  #next one existing
                self._selection.select_iter(itr2)
                self._removeLayout(store, itr)
                # Re-emit the selection changed signal now that the backing store is updated
                # in order to update the first/last/only-based button sensitivities
                self._selection.emit("changed")
                return

            #nothing left, run AddLayout dialog to replace the current layout
            #add it to GLib.idle to make sure the underlaying gui is correctly
            #redrawn
            self._remove_last_attempt = True
            add_button = self.builder.get_object("addLayoutButton")
            gtk_call_once(self.on_add_clicked, add_button)
            return

        #the selected item is not the first, select the previous one
        #XXX: there is no model.iter_previous() so we have to find it this way
        itr3 = store.iter_next(itr2)  #look-ahead iterator
        while itr3 and (store[itr3][0] != store[itr][0]):
            itr2 = store.iter_next(itr2)
            itr3 = store.iter_next(itr3)

        self._removeLayout(store, itr)
        self._selection.select_iter(itr2)

    def on_up_clicked(self, button):
        if not self._selection.count_selected_rows():
            return

        (store, cur) = self._selection.get_selected()
        prev = store.iter_previous(cur)
        if not prev:
            return

        store.swap(cur, prev)
        if conf.system.can_configure_keyboard:
            self._flush_layouts_to_X()

        if not store.iter_previous(cur):
            #layout is first in the list (set as default), activate it
            self._xkl_wrapper.activate_default_layout()

        self._selection.emit("changed")

    def on_down_clicked(self, button):
        if not self._selection.count_selected_rows():
            return

        (store, cur) = self._selection.get_selected()

        #if default layout (first in the list) changes we need to activate it
        activate_default = not store.iter_previous(cur)

        nxt = store.iter_next(cur)
        if not nxt:
            return

        store.swap(cur, nxt)
        if conf.system.can_configure_keyboard:
            self._flush_layouts_to_X()

        if activate_default:
            self._xkl_wrapper.activate_default_layout()

        self._selection.emit("changed")

    def on_preview_clicked(self, button):
        (store, cur) = self._selection.get_selected()
        layout_row = store[cur]
        if not layout_row:
            return

        layout, variant = keyboard.parse_layout_variant(layout_row[0])

        if variant:
            lay_var_spec = "%s\t%s" % (layout, variant)
        else:
            lay_var_spec = layout

        dialog = Gkbd.KeyboardDrawing.dialog_new()
        Gkbd.KeyboardDrawing.dialog_set_layout(dialog,
                                               self._xkl_wrapper.configreg,
                                               lay_var_spec)
        dialog.set_size_request(750, 350)
        dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
        with self.main_window.enlightbox(dialog):
            dialog.show_all()
            dialog.run()
            dialog.destroy()

    def on_selection_changed(self, selection, *args):
        # We don't have to worry about multiple rows being selected in this
        # function, because that's disabled by the widget.
        if not selection.count_selected_rows():
            self._upButton.set_sensitive(False)
            self._downButton.set_sensitive(False)
            self._removeButton.set_sensitive(False)
            self._previewButton.set_sensitive(False)
            return

        (store, selected) = selection.get_selected_rows()

        # If something's selected, always enable the remove and preview buttons.
        self._removeButton.set_sensitive(True)
        self._previewButton.set_sensitive(True)

        # If only one row is available, disable both the Up and Down button
        if len(store) == 1:
            self._upButton.set_sensitive(False)
            self._downButton.set_sensitive(False)
        else:
            # Disable the Up button if the top row's selected, and disable the
            # Down button if the bottom row's selected.
            if selected[0].get_indices() == [0]:
                self._upButton.set_sensitive(False)
                self._downButton.set_sensitive(True)
            elif selected[0].get_indices() == [len(store) - 1]:
                self._upButton.set_sensitive(True)
                self._downButton.set_sensitive(False)
            else:
                self._upButton.set_sensitive(True)
                self._downButton.set_sensitive(True)

    def on_options_clicked(self, *args):
        self._switching_dialog.refresh()

        with self.main_window.enlightbox(self._switching_dialog.window):
            response = self._switching_dialog.run()

        if response != 1:
            # Cancel clicked, dialog destroyed
            return

        # OK clicked, set and save switching options.
        new_options = self._switching_dialog.checked_options
        self._xkl_wrapper.set_switching_options(new_options)
        self._l12_module.SetLayoutSwitchOptions(new_options)

        # Refresh switching info label.
        self._refresh_switching_info()

    def _add_data_layouts(self):
        if not self._l12_module.XLayouts:
            # nothing specified, just add the default
            self._addLayout(self._store, DEFAULT_KEYBOARD)
            return

        valid_layouts = []
        for layout in self._l12_module.XLayouts:
            try:
                self._addLayout(self._store, layout)
                valid_layouts += layout
            except XklWrapperError:
                log.error("Failed to add layout '%s'", layout)

        if not valid_layouts:
            log.error("No valid layout given, falling back to default %s",
                      DEFAULT_KEYBOARD)
            self._addLayout(self._store, DEFAULT_KEYBOARD)
            self._l12_module.SetXLayouts([DEFAULT_KEYBOARD])

    def _flush_layouts_to_X(self):
        layouts_list = list()

        for row in self._store:
            layouts_list.append(row[0])

        self._xkl_wrapper.replace_layouts(layouts_list)
Пример #5
0
class SubscriptionSpoke(NormalSpoke):
    """Subscription spoke provides the Connect to Red Hat screen."""
    builderObjects = ["subscription_window"]

    mainWidgetName = "subscription_window"
    uiFile = "spokes/subscription.glade"

    category = SoftwareCategory

    icon = "application-certificate-symbolic"
    title = CN_("GUI|Spoke", "_Connect to Red Hat")

    # main notebook pages
    REGISTRATION_PAGE = 0
    SUBSCRIPTION_STATUS_PAGE = 1

    @classmethod
    def should_run(cls, environment, data):
        """The Subscription spoke should run only if the Subscription module is available."""
        return is_module_available(SUBSCRIPTION)

    def __init__(self, *args):
        super().__init__(*args)

        # connect to the Subscription DBus module API
        self._subscription_module = SUBSCRIPTION.get_proxy()

        # connect to the Network DBus module API
        self._network_module = NETWORK.get_proxy()

        # get initial data from the Subscription module
        self._subscription_request = self._get_subscription_request()
        self._system_purpose_data = self._get_system_purpose_data()
        # Keep a copy of system purpose data that has been last applied to
        # the installation environment.
        # That way we can check if the main copy of the system purposed data
        # changed since it was applied (for example due to user input)
        # and needs to be reapplied.
        # By default this variable is None and will only be set to a
        # SystemPurposeData instance when first system purpose data is
        # applied to the installation environment.
        self._last_applied_system_purpose_data = None

        self._authentication_method = AuthenticationMethod.USERNAME_PASSWORD

        self._registration_error = ""
        self._registration_phase = None
        self._registration_controls_enabled = True

        # Red Hat Insights should be enabled by default for non-kickstart installs.
        #
        # For kickstart installations we will use the value from the module, which
        # False by default & can be set to True via the rhsm kickstart command.
        if not flags.automatedInstall:
            self._subscription_module.SetInsightsEnabled(True)

        # previous visit network connectivity tracking
        self._network_connected_previously = False

        # overriden source tracking
        self._overridden_source_type = None

    # common spoke properties

    @property
    def ready(self):
        """The subscription spoke is ready once its initialization thread finishes.

        We do this to avoid the Subscription spoke being set mandatory in cases
        where the current installation source is the CDN, but payload refresh is still
        running and it might change to CDROM later one. We achieve this by waiting
        for tha payload refresh thread to finish in the Subscription spoke initialization
        thread.
        """
        return not threadMgr.get(THREAD_SUBSCRIPTION_SPOKE_INIT)

    @property
    def status(self):
        # The spoke status message:
        # - shows registration phases when registration + subscription
        #   or unregistration is ongoing
        # - otherwise shows not-registered/registered/error
        return self._get_status_message()

    @property
    def mandatory(self):
        """The subscription spoke is mandatory if Red Hat CDN is set as installation source."""
        return check_cdn_is_installation_source(self.payload)

    @property
    def completed(self):
        return self.subscription_attached

    @property
    def sensitive(self):
        # the Subscription spoke should be always accessible
        return True

    # common spoke methods

    def apply(self):
        log.debug("Subscription GUI: apply() running")
        self._set_data_to_module()

    def refresh(self):
        log.debug("Subscription GUI: refresh() running")
        # update spoke state based on up-to-date data from the Subscription module
        # (this also takes care of updating the two properties holding subscription
        #  request as well as system purpose data)
        self._update_spoke_state()
        # check if network connectivity is available
        # - without network connectivity the spoke is pretty much unusable
        # - also, no need to check if registration/unregistration is in progress
        if not self.registration_phase:
            self._check_connectivity()

    # DBus structure mirrors

    @property
    def subscription_request(self):
        """A mirror of the subscription request from the Subscription DBus module.

        Should be always set and is periodically updated on refresh().

        :return: up to date subscription request
        :rtype: SubscriptionRequest instance
        """
        return self._subscription_request

    @property
    def system_purpose_data(self):
        """A mirror of system purpose data from the Subscription DBus module.

        Should be always set and is periodically updated on refresh().

        :return: up to date system purpose data
        :rtype: SystemPurposeData instance
        """
        return self._system_purpose_data

    # placeholder control

    def enable_http_proxy_password_placeholder(self, show_placeholder):
        """Show a placeholder on the HTTP proxy password field.

        The placeholder notifies the user about HTTP proxy password
        being set in the DBus module.

        The placeholder will be only shown if there is no
        actual text in the entry field.
        """
        if show_placeholder:
            self._http_proxy_password_entry.set_placeholder_text(
                _("Password set."))
        else:
            self._http_proxy_password_entry.set_placeholder_text("")

    def enable_password_placeholder(self, show_placeholder):
        """Show a placeholder on the red hat account password field.

        The placeholder notifies the user about activation
        key being set in the DBus module.

        The placeholder will be only shown if there is no
        actual text in the entry field.
        """
        if show_placeholder:
            self._password_entry.set_placeholder_text(_("Password set."))
        else:
            self._password_entry.set_placeholder_text("")

    def enable_activation_key_placeholder(self, show_placeholder):
        """Show a placeholder on the activation key field.

        The placeholder notifies the user about activation
        key being set in the DBus module.

        The placeholder will be only shown if there is no
        actual text in the entry field.
        """
        if show_placeholder:
            self._activation_key_entry.set_placeholder_text(
                _("Activation key set."))
        else:
            self._activation_key_entry.set_placeholder_text("")

    # properties controlling visibility of options that can be hidden

    @property
    def custom_server_hostname_visible(self):
        return self._custom_server_hostname_checkbox.get_active()

    @custom_server_hostname_visible.setter
    def custom_server_hostname_visible(self, visible):
        self._custom_server_hostname_checkbox.set_active(visible)

    @property
    def http_proxy_visible(self):
        return self._http_proxy_checkbox.get_active()

    @http_proxy_visible.setter
    def http_proxy_visible(self, visible):
        self._http_proxy_checkbox.set_active(visible)

    @property
    def custom_rhsm_baseurl_visible(self):
        return self._custom_rhsm_baseurl_checkbox.get_active()

    @custom_rhsm_baseurl_visible.setter
    def custom_rhsm_baseurl_visible(self, visible):
        self._custom_rhsm_baseurl_checkbox.set_active(visible)

    def set_account_visible(self, visible):
        self._account_radio_button.set_active(visible)

    def set_activation_key_visible(self, visible):
        self._activation_key_radio_button.set_active(visible)

    def set_system_purpose_visible(self, visible):
        self._system_purpose_checkbox.set_active(visible)

    def set_options_visible(self, visible):
        self._options_expander.set_expanded(visible)

    # properties - element sensitivity

    def set_registration_controls_sensitive(self, sensitive):
        """Set sensitivity of the registration controls.

        We set these value individually so that the registration status label
        that is between the controls will not become grayed out due to setting
        the top level container insensitive.
        """
        self._registration_grid.set_sensitive(sensitive)
        self._options_expander.set_sensitive(sensitive)
        self._registration_controls_enabled = sensitive
        self._update_registration_state()

    # authentication related signals

    def on_account_radio_button_toggled(self, radio):
        self._account_revealer.set_reveal_child(radio.get_active())
        if radio.get_active():
            self.authentication_method = AuthenticationMethod.USERNAME_PASSWORD

    def on_activation_key_radio_button_toggled(self, radio):
        self._activation_key_revealer.set_reveal_child(radio.get_active())
        if radio.get_active():
            self.authentication_method = AuthenticationMethod.ORG_KEY

    def on_username_entry_changed(self, editable):
        self.subscription_request.account_username = editable.get_text()
        self._update_registration_state()

    def on_password_entry_changed(self, editable):
        entered_text = editable.get_text()
        if entered_text:
            self.enable_password_placeholder(False)
        self.subscription_request.account_password.set_secret(entered_text)
        self._update_registration_state()

    def on_organization_entry_changed(self, editable):
        self.subscription_request.organization = editable.get_text()
        self._update_registration_state()

    def on_activation_key_entry_changed(self, editable):
        entered_text = editable.get_text()
        keys = None
        if entered_text:
            self.enable_activation_key_placeholder(False)
            keys = entered_text.split(',')
        # keys == None clears keys in the module, so deleting keys
        # in the keys field will also clear module data on apply()
        self.subscription_request.activation_keys.set_secret(keys)
        self._update_registration_state()

    # system purpose related signals

    def on_system_purpose_checkbox_toggled(self, checkbox):
        active = checkbox.get_active()
        self._system_purpose_revealer.set_reveal_child(active)
        if active:
            # make sure data in the system purpose comboboxes
            # are forwarded to the system purpose data structure
            # in case something was set before they were hidden
            self.on_system_purpose_role_combobox_changed(
                self._system_purpose_role_combobox)
            self.on_system_purpose_sla_combobox_changed(
                self._system_purpose_sla_combobox)
            self.on_system_purpose_usage_combobox_changed(
                self._system_purpose_usage_combobox)
        else:
            # system purpose combo boxes have been hidden, clear the corresponding
            # data from the system purpose data structure, but keep it in the combo boxes
            # in case the user tries to show them again before next spoke entry clears them
            self.system_purpose_data.role = ""
            self.system_purpose_data.sla = ""
            self.system_purpose_data.usage = ""

    def on_system_purpose_role_combobox_changed(self, combobox):
        self.system_purpose_data.role = combobox.get_active_id()

    def on_system_purpose_sla_combobox_changed(self, combobox):
        self.system_purpose_data.sla = combobox.get_active_id()

    def on_system_purpose_usage_combobox_changed(self, combobox):
        self.system_purpose_data.usage = combobox.get_active_id()

    # HTTP proxy signals

    def on_http_proxy_checkbox_toggled(self, checkbox):
        active = checkbox.get_active()
        self._http_proxy_revealer.set_reveal_child(active)
        if active:
            # make sure data in the HTTP proxy entries
            # are forwarded to the subscription request structure
            # in case something was entered before they were hidden
            self.on_http_proxy_location_entry_changed(
                self._http_proxy_location_entry)
            self.on_http_proxy_username_entry_changed(
                self._http_proxy_username_entry)
            self.on_http_proxy_password_entry_changed(
                self._http_proxy_password_entry)
        else:
            # HTTP proxy entries have been hidden, clear the corresponding data from
            # the subscription request structure, but keep it in the entries in case
            # the user tries to show them again before next spoke entry clears them
            self._subscription_request.server_proxy_hostname = ""
            self._subscription_request.server_proxy_port = -1
            self._subscription_request.server_proxy_user = ""
            self._subscription_request.server_proxy_password.set_secret(None)

    def on_http_proxy_location_entry_changed(self, editable):
        # Incorrect hostnames, including empty strings, will
        # throw an exception we need to catch and switch
        # to defaults. This can happen often as the user
        # types the hostname to the field.
        try:
            port = -1  # not set == -1
            proxy_obj = ProxyString(url=editable.get_text())
            hostname = proxy_obj.host
            if proxy_obj.port:
                # the DBus API expects an integer
                port = int(proxy_obj.port)
        except ProxyStringError:
            hostname = ""
        # set the resulting values to the DBus structure
        self.subscription_request.server_proxy_hostname = hostname
        self.subscription_request.server_proxy_port = port

    def on_http_proxy_username_entry_changed(self, editable):
        self.subscription_request.server_proxy_user = editable.get_text()

    def on_http_proxy_password_entry_changed(self, editable):
        password = editable.get_text()
        # if password is set in the field, set it, or set None to clear the password
        self.subscription_request.server_proxy_password.set_secret(password
                                                                   or None)

    # custom server hostname and rhsm baseurl signals

    def on_custom_server_hostname_checkbox_toggled(self, checkbox):
        active = checkbox.get_active()
        self._custom_server_hostname_revealer.set_reveal_child(active)
        if active:
            # make sure data in the server hostname entry
            # is forwarded to the subscription request structure
            # in case something was entered before the entry was
            # hidden
            self.on_custom_server_hostname_entry_changed(
                self._custom_server_hostname_entry)
        else:
            # the entry was hidden, clear the data from subscription request but
            # keep it in the entry in case user decides to show the entry again
            # before next spoke entry clears it
            self.subscription_request.server_hostname = ""

    def on_custom_server_hostname_entry_changed(self, editable):
        self.subscription_request.server_hostname = editable.get_text()

    def on_custom_rhsm_baseurl_checkbox_toggled(self, checkbox):
        active = checkbox.get_active()
        self._custom_rhsm_baseurl_revealer.set_reveal_child(active)
        if active:
            # make sure data in the rhsm baseurl entry
            # is forwarded to the subscription request structure
            # in case something was entered before the entry was
            # hidden
            self.on_custom_rhsm_baseurl_entry_changed(
                self._custom_rhsm_baseurl_entry)
        else:
            # the entry was hidden, clear the data from subscription request but
            # keep it in the entry in case user decides to show the entry again
            # before next spoke entry clears it
            self.subscription_request.rhsm_baseurl = ""

    def on_custom_rhsm_baseurl_entry_changed(self, editable):
        self.subscription_request.rhsm_baseurl = editable.get_text()

    # button signals

    def on_register_button_clicked(self, button):
        log.debug("Subscription GUI: register button clicked")
        self._register()

    def on_unregister_button_clicked(self, button):
        """Handle registration related tasks."""
        log.debug("Subscription GUI: unregister button clicked")
        self._unregister()

    # properties - general properties

    @property
    def registration_phase(self):
        """Reports what phase the registration procedure is in.

        Only valid if a registration thread is running.
        """
        return self._registration_phase

    @registration_phase.setter
    def registration_phase(self, phase):
        self._registration_phase = phase

    @property
    def subscription_attached(self):
        """Was a subscription entitlement successfully attached ?"""
        return self._subscription_module.IsSubscriptionAttached

    @property
    def network_connected(self):
        """Does it look like that we have network connectivity ?

        Network connectivity is required for subscribing a system.
        """
        return self._network_module.Connected

    @property
    def authentication_method(self):
        """Report which authentication method is in use."""
        return self._authentication_method

    @authentication_method.setter
    def authentication_method(self, method):
        self._authentication_method = method
        if method == AuthenticationMethod.USERNAME_PASSWORD:
            self.set_activation_key_visible(False)
            self.set_account_visible(True)
            self.subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD
        elif method == AuthenticationMethod.ORG_KEY:
            self.set_activation_key_visible(True)
            self.set_account_visible(False)
            self.subscription_request.type = SUBSCRIPTION_REQUEST_TYPE_ORG_KEY

    @property
    def options_set(self):
        """Report if at least one option in the Options section has been set."""
        return self.http_proxy_visible or self.custom_server_hostname_visible or \
            self.custom_rhsm_baseurl_visible

    @property
    def registration_error(self):
        return self._registration_error

    @registration_error.setter
    def registration_error(self, error_message):
        self._registration_error = error_message
        # also set the spoke warning banner
        self.show_warning_message(error_message)

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()

        # get object references from the builders
        self._main_notebook = self.builder.get_object("main_notebook")

        # * the registration tab  * #

        # container for the main registration controls
        self._registration_grid = self.builder.get_object("registration_grid")

        # authentication
        self._account_radio_button = self.builder.get_object(
            "account_radio_button")
        self._activation_key_radio_button = self.builder.get_object(
            "activation_key_radio_button")

        # authentication - account
        self._account_revealer = self.builder.get_object("account_revealer")
        self._username_entry = self.builder.get_object("username_entry")
        self._password_entry = self.builder.get_object("password_entry")

        # authentication - activation key
        self._activation_key_revealer = self.builder.get_object(
            "activation_key_revealer")
        self._organization_entry = self.builder.get_object(
            "organization_entry")
        self._activation_key_entry = self.builder.get_object(
            "activation_key_entry")

        # system purpose
        self._system_purpose_checkbox = self.builder.get_object(
            "system_purpose_checkbox")
        self._system_purpose_revealer = self.builder.get_object(
            "system_purpose_revealer")
        self._system_purpose_role_combobox = self.builder.get_object(
            "system_purpose_role_combobox")
        self._system_purpose_sla_combobox = self.builder.get_object(
            "system_purpose_sla_combobox")
        self._system_purpose_usage_combobox = self.builder.get_object(
            "system_purpose_usage_combobox")

        # insights
        self._insights_checkbox = self.builder.get_object("insights_checkbox")

        # options expander
        self._options_expander = self.builder.get_object("options_expander")

        # HTTP proxy
        self._http_proxy_checkbox = self.builder.get_object(
            "http_proxy_checkbox")
        self._http_proxy_revealer = self.builder.get_object(
            "http_proxy_revealer")
        self._http_proxy_location_entry = self.builder.get_object(
            "http_proxy_location_entry")
        self._http_proxy_username_entry = self.builder.get_object(
            "http_proxy_username_entry")
        self._http_proxy_password_entry = self.builder.get_object(
            "http_proxy_password_entry")

        # RHSM baseurl
        self._custom_rhsm_baseurl_checkbox = self.builder.get_object(
            "custom_rhsm_baseurl_checkbox")
        self._custom_rhsm_baseurl_revealer = self.builder.get_object(
            "custom_rhsm_baseurl_revealer")
        self._custom_rhsm_baseurl_entry = self.builder.get_object(
            "custom_rhsm_baseurl_entry")

        # server hostname
        self._custom_server_hostname_checkbox = self.builder.get_object(
            "custom_server_hostname_checkbox")
        self._custom_server_hostname_revealer = self.builder.get_object(
            "custom_server_hostname_revealer")
        self._custom_server_hostname_entry = self.builder.get_object(
            "custom_server_hostname_entry")

        # status label
        self._registration_status_label = self.builder.get_object(
            "registration_status_label")

        # register button
        self._register_button = self.builder.get_object("register_button")

        # * the subscription status tab * #

        # general status
        self._method_status_label = self.builder.get_object(
            "method_status_label")
        self._role_status_label = self.builder.get_object("role_status_label")
        self._sla_status_label = self.builder.get_object("sla_status_label")
        self._usage_status_label = self.builder.get_object(
            "usage_status_label")
        self._insights_status_label = self.builder.get_object(
            "insights_status_label")

        # attached subscriptions
        self._attached_subscriptions_label = self.builder.get_object(
            "attached_subscriptions_label")
        self._subscriptions_listbox = self.builder.get_object(
            "subscriptions_listbox")

        # setup spoke state based on data from the Subscription DBus module
        self._update_spoke_state()

        # start the rest of spoke initialization which might take some time
        # (mainly due to waiting for various initialization threads to finish)
        # in a separate thread
        threadMgr.add(
            AnacondaThread(name=THREAD_SUBSCRIPTION_SPOKE_INIT,
                           target=self._initialize))

    def _initialize(self):
        # wait for subscription thread to finish (if any)
        threadMgr.wait(THREAD_SUBSCRIPTION)
        # also wait for the payload thread, which migh still be processing
        # a CDROM source, to avoid the Subscription being mandatory by mistake
        # due to CDN still being default at the time of evaulation
        threadMgr.wait(THREAD_PAYLOAD)

        # update overall state
        self._update_registration_state()
        self._update_subscription_state()

        # Send ready signal to main event loop
        hubQ.send_ready(self.__class__.__name__)

        # report that we are done
        self.initialize_done()

    # private methods

    def _update_spoke_state(self):
        """Setup spoke state based on Subscription DBus module state.

        Subscription DBus module state is represented by the SubscriptionRequest and
        SystemPurposeData DBus structures. We first update their local mirrors from
        the DBus module and then set all the controls in the spoke to values
        represented in the DBus structures.

        NOTE: There are a couple special cases where we need to do some special precessing,
              such as for fields holding sensitive data. If we blindly set those based
              on DBus structure data, we would effectively clear them as the Subscription
              DBus module never returns previously set sensitive data in plain text.

        """
        # start by pulling in fresh data from the Subscription DBus module
        self._subscription_request = self._get_subscription_request()
        self._system_purpose_data = self._get_system_purpose_data()

        # next update the authentication part of the UI
        self._update_authetication_ui()

        # check if system purpose part of the spoke should be visible
        self.set_system_purpose_visible(
            self.system_purpose_data.check_data_available())

        # NOTE: the fill_combobox() function makes sure to remove old data from the
        #       combo box before filling it

        # role
        fill_combobox(self._system_purpose_role_combobox,
                      self.system_purpose_data.role,
                      self._subscription_module.GetValidRoles())
        # SLA
        fill_combobox(self._system_purpose_sla_combobox,
                      self.system_purpose_data.sla,
                      self._subscription_module.GetValidSLAs())
        # usage
        fill_combobox(self._system_purpose_usage_combobox,
                      self.system_purpose_data.usage,
                      self._subscription_module.GetValidUsageTypes())

        # Insights
        self._insights_checkbox.set_active(
            self._subscription_module.InsightsEnabled)

        # update the HTTP proxy part of the UI
        self._update_http_proxy_ui()

        # set custom server hostname
        self.custom_server_hostname_visible = bool(
            self.subscription_request.server_hostname)
        self._custom_server_hostname_entry.set_text(
            self.subscription_request.server_hostname)

        # set custom rhsm baseurl
        self.custom_rhsm_baseurl_visible = bool(
            self.subscription_request.rhsm_baseurl)
        self._custom_rhsm_baseurl_entry.set_text(
            self.subscription_request.rhsm_baseurl)

        # if there is something set in the Options section, expand the expander
        # - this needs to go last, after all the values in option section are set/not set
        if self.options_set:
            self.set_options_visible(True)

        # now that we updated the spoke with fresh data from the module, we can run the
        # general purpose update functions that make sure the two parts of the spoke
        # (the registration part and the subscription part) are both valid
        self._update_registration_state()
        self._update_subscription_state()

    def _update_authetication_ui(self):
        """Update the authentication part of the spoke.

        - SubscriptionRequest always has type set
        - username + password is the default
        For the related password and activation keys entry holding sensitive data
        we need to reconcile the data held in the spoke from previous entry with
        data set in the DBus module previously:
        - data in module and entry empty -> set placeholder
        - data in module and entry populated -> keep text in entry,
          we assume it is the same as what is in module
        - no data in module and entry populated -> clear entry & any placeholders
          (data cleared over DBus API)
        - no data in module and entry empty -> do nothing
        """
        if self.subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_USERNAME_PASSWORD:
            self.authentication_method = AuthenticationMethod.USERNAME_PASSWORD
            self._username_entry.set_text(
                self.subscription_request.account_username)
            set_in_entry = bool(self._password_entry.get_text())
            set_in_module = self.subscription_request.account_password.type == SECRET_TYPE_HIDDEN
            if set_in_module:
                if not set_in_entry:
                    self.enable_password_placeholder(True)
            else:
                self._password_entry.set_text("")
                self.enable_password_placeholder(False)
        elif self.subscription_request.type == SUBSCRIPTION_REQUEST_TYPE_ORG_KEY:
            self.authentication_method = AuthenticationMethod.ORG_KEY
            self._organization_entry.set_text(
                self.subscription_request.organization)
            set_in_entry = bool(self._activation_key_entry.get_text())
            set_in_module = self.subscription_request.activation_keys.type == SECRET_TYPE_HIDDEN
            if set_in_module:
                if not set_in_entry:
                    self.enable_activation_key_placeholder(True)
            else:
                self._activation_key_entry.set_text("")
                self.enable_activation_key_placeholder(False)

    def _update_http_proxy_ui(self):
        """Update the HTTP proxy configuration part of the spoke."""
        proxy_hostname = self.subscription_request.server_proxy_hostname
        proxy_port = self.subscription_request.server_proxy_port
        proxy_port_set = proxy_port >= 0
        proxy_username = self.subscription_request.server_proxy_user
        proxy_password_secret = self.subscription_request.server_proxy_password
        proxy_password_set = proxy_password_secret.type == SECRET_TYPE_HIDDEN
        self.http_proxy_visible = proxy_hostname or proxy_username or proxy_password_set
        if proxy_hostname:
            proxy_url = proxy_hostname
            if proxy_port_set:
                proxy_url = "{}:{}".format(proxy_url, proxy_port)
            self._http_proxy_location_entry.set_text(proxy_url)
        # HTTP proxy username
        self._http_proxy_username_entry.set_text(proxy_username)
        # HTTP proxy password
        set_in_entry = bool(self._http_proxy_password_entry.get_text())
        secret_type = self.subscription_request.server_proxy_password.type
        set_in_module = secret_type == SECRET_TYPE_HIDDEN
        if set_in_module:
            if not set_in_entry:
                self.enable_http_proxy_password_placeholder(True)
        else:
            self._http_proxy_password_entry.set_text("")
            self.enable_http_proxy_password_placeholder(False)

    def _set_data_to_module(self):
        """Set system purpose data to the DBus module.

        Called either on apply() or right before a subscription
        attempt.
        """
        self._set_system_purpose_data()
        # Set data about Insights to the DBus module.
        self._set_insights()
        # Set subscription request to the DBus module.
        self._set_subscription_request()

    def _get_system_purpose_data(self):
        """Get SystemPurposeData from the Subscription module."""
        struct = self._subscription_module.SystemPurposeData
        return SystemPurposeData.from_structure(struct)

    def _set_system_purpose_data(self):
        """Set system purpose data to the Subscription DBus module."""
        self._subscription_module.SetSystemPurposeData(
            SystemPurposeData.to_structure(self.system_purpose_data))
        # also apply the data (only applies when needed)
        self._apply_system_purpose_data()

    def _apply_system_purpose_data(self):
        """Apply system purpose data to the installation environment.

        Apply system purpose data to the installation environment, provided that:
        - system purpose data has not yet been applied to the system
        or
        - current system purpose data is different from the data last applied to the system

        Due to that we keep a copy of the last applied system purpose data so that we can
        check for difference.

        If the last applied data is the same as current system purpose data, nothing is done.
        """
        if self._last_applied_system_purpose_data != self.system_purpose_data:
            log.debug(
                "Subscription GUI: applying system purpose data to installation environment"
            )
            task_path = self._subscription_module.SetSystemPurposeWithTask()
            task_proxy = SUBSCRIPTION.get_proxy(task_path)
            sync_run_task(task_proxy)
            self._last_applied_system_purpose_data = self.system_purpose_data

    def _get_subscription_request(self):
        """Get SubscriptionRequest from the Subscription module."""
        struct = self._subscription_module.SubscriptionRequest
        return SubscriptionRequest.from_structure(struct)

    def _set_subscription_request(self):
        """Set subscription request to the Subscription DBus module."""
        self._subscription_module.SetSubscriptionRequest(
            SubscriptionRequest.to_structure(self.subscription_request))

    def _set_insights(self):
        """Configure Insights in DBus module based on GUI state."""
        self._subscription_module.SetInsightsEnabled(
            self._insights_checkbox.get_active())

    def _register(self):
        """Try to register a system."""
        # update data in the Subscription DBUS module
        self._set_data_to_module()

        # disable controls
        self.set_registration_controls_sensitive(False)

        # wait for the previous subscription thread to finish
        threadMgr.wait(THREAD_SUBSCRIPTION)

        # check if the current installation source will be overriden
        # and remember it if it is the case
        source_proxy = self.payload.get_source_proxy()
        source_type = source_proxy.Type
        if source_type in SOURCE_TYPES_OVERRIDEN_BY_CDN:
            self._overridden_source_type = source_type
        else:
            # no override will happen, so clear the variable
            self._overridden_source_type = None

        # try to register
        log.debug("Subscription GUI: attempting to register")
        threadMgr.add(
            AnacondaThread(name=THREAD_SUBSCRIPTION,
                           target=register_and_subscribe,
                           kwargs={
                               "payload": self.payload,
                               "progress_callback":
                               self._subscription_progress_callback,
                               "error_callback":
                               self._subscription_error_callback,
                               "restart_payload": True
                           }))

    def _unregister(self):
        """Try to unregister a system."""
        # update data in the Subscription DBUS module
        self._set_data_to_module()

        # disable controls
        self.set_registration_controls_sensitive(False)

        # wait for the previous subscription thread to finish
        threadMgr.wait(THREAD_SUBSCRIPTION)

        # try to unregister
        log.debug("Subscription GUI: attempting to unregister")
        threadMgr.add(
            AnacondaThread(name=THREAD_SUBSCRIPTION,
                           target=unregister,
                           kwargs={
                               "payload": self.payload,
                               "overridden_source_type":
                               self._overridden_source_type,
                               "progress_callback":
                               self._subscription_progress_callback,
                               "error_callback":
                               self._subscription_error_callback,
                               "restart_payload": True
                           }))

    @async_action_wait
    def _subscription_progress_callback(self, phase):
        """Progress handling for subscription thread.

        Used both for both registration + attaching subscription
        and for unregistration.

        NOTE: Using the @async_action_wait decorator as this is
              called from the subscription thread. We need to do
              that as GTK does bad things if non main threads
              interact with it.
        """
        # clear error message from a previous attempt (if any)
        self.registration_error = ""
        # set registration phase
        self.registration_phase = phase

        # set spoke status according to subscription thread phase
        if phase == SubscriptionPhase.DONE:
            log.debug("Subscription GUI: registration & attach done")
            # we are done, clear the phase
            self.registration_phase = None
            # update registration and subscription parts of the spoke
            self._update_registration_state()
            self._update_subscription_state()
            # enable controls
            self.set_registration_controls_sensitive(True)
            # notify hub
            hubQ.send_ready(self.__class__.__name__)
        else:
            # processing still ongoing, set the phase
            self.registration_phase = phase
            # notify hub
            hubQ.send_ready(self.__class__.__name__)
        # update spoke state
        self._update_registration_state()

    @async_action_wait
    def _subscription_error_callback(self, error_message):
        log.debug("Subscription GUI: registration & attach failed")
        # store the error message
        self.registration_error = error_message
        # even if we fail, we are technically done,
        # so clear the phase
        self.registration_phase = None
        # update registration and subscription parts of the spoke
        self._update_registration_state()
        self._update_subscription_state()
        # re-enable controls, so user can try again
        self.set_registration_controls_sensitive(True)
        # notify hub
        hubQ.send_ready(self.__class__.__name__)

    def _get_status_message(self):
        """Get status message describing current spoke state.

        The registration phase is taken into account (if any)
        as well as possible error state and subscription
        being or not being attached.

        NOTE: This method is used both for the spoke status message
              as well as for the in-spoke status label.
        """
        phase = self.registration_phase
        if phase:
            if phase == SubscriptionPhase.UNREGISTER:
                return _("Unregistering...")
            elif phase == SubscriptionPhase.REGISTER:
                return _("Registering...")
            elif phase == SubscriptionPhase.ATTACH_SUBSCRIPTION:
                return _("Attaching subscription...")
            elif phase == SubscriptionPhase.DONE:
                return _("Subscription attached.")
        elif self.registration_error:
            return _("Registration failed.")
        elif self.subscription_attached:
            return _("Registered.")
        else:
            return _("Not registered.")

    @async_action_wait
    def _update_registration_state(self):
        """Update state of the registration related part of the spoke.

        Hopefully this method is not too inefficient as it is running basically
        on every keystroke in the username/password/organization/key entry.
        """
        subscription_attached = self.subscription_attached
        if subscription_attached:
            self._main_notebook.set_current_page(self.SUBSCRIPTION_STATUS_PAGE)
        else:
            self._main_notebook.set_current_page(self.REGISTRATION_PAGE)

        # update registration status label
        self._registration_status_label.set_text(self._get_status_message())

        # update registration button state
        self._update_register_button_state()

    @async_action_wait
    def _update_subscription_state(self):
        """Update state of the subscription related part of the spoke.

        Update state of the part of the spoke, that shows data about the
        currently attached subscriptions.
        """
        # authentication method
        if self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD:
            method_string = _("Registered with account {}").format(
                self.subscription_request.account_username)
        else:  # org + key
            method_string = _("Registered with organization {}").format(
                self.subscription_request.organization)
        self._method_status_label.set_text(method_string)

        # final syspurpose data

        # role
        final_role_string = _("Role: {}").format(self.system_purpose_data.role)
        self._role_status_label.set_text(final_role_string)

        # SLA
        final_sla_string = _("SLA: {}").format(self.system_purpose_data.sla)
        self._sla_status_label.set_text(final_sla_string)

        # usage
        final_usage_string = _("Usage: {}").format(
            self.system_purpose_data.usage)
        self._usage_status_label.set_text(final_usage_string)

        # Insights
        # - this strings are referring to the desired target system state,
        #   the installation environment itself is not expected to be
        #   connected to Insights
        if self._subscription_module.InsightsEnabled:
            insights_string = _("Connected to Red Hat Insights")
        else:
            insights_string = _("Not connected to Red Hat Insights")
        self._insights_status_label.set_text(insights_string)

        # get attached subscriptions as a list of structs
        attached_subscriptions = self._subscription_module.AttachedSubscriptions
        # turn the structs to more useful AttachedSubscription instances
        attached_subscriptions = AttachedSubscription.from_structure_list(
            attached_subscriptions)

        # check how many we have & set the subscription status string accordingly
        subscription_count = len(attached_subscriptions)
        if subscription_count == 0:
            subscription_string = _(
                "No subscriptions are attached to the system")
        elif subscription_count == 1:
            subscription_string = _("1 subscription attached to the system")
        else:
            subscription_string = _("{} subscriptions attached to the system"
                                    ).format(subscription_count)

        self._attached_subscriptions_label.set_text(subscription_string)

        # populate the attached subscriptions listbox
        populate_attached_subscriptions_listbox(self._subscriptions_listbox,
                                                attached_subscriptions)

    def _check_connectivity(self):
        """Check network connectivity is available.

        Network connectivity is required for using the Subscription spoke
        for obvious reasons (eq. for communication with the remote
        Candlepin instance & CDN).

        If network is already available, this method makes the registration
        controls sensitive and clears any previous connectivity warnings.

        If network is not available it makes the registration controls
        insensitive and displays a warning to the user.
        """
        network_connected = self.network_connected
        if network_connected:
            # make controls sensitive, unless processing is ongoing
            self.set_registration_controls_sensitive(True)
            if not self._network_connected_previously:
                # clear previous connectivity warning
                # - we only do this on connectivity state change so that we don't clear
                #   registration error related warnings
                log.debug("Subscription GUI: clearing connectivity warning")
                self.clear_info()
        else:
            # make controls insensitive
            self.set_registration_controls_sensitive(False)
            # set a warning
            log.debug("Subscription GUI: setting connectivity warning")
            self.show_warning_message(
                _("Please enable network access before connecting to Red Hat.")
            )
        # remember state
        self._network_connected_previously = network_connected

    def _update_register_button_state(self):
        """Update register button state.

        The button is only sensitive if no processing is ongoing
        and we either have enough authentication data to register
        or the system is subscribed, so we can unregister it.
        """
        button_sensitive = False
        if self._registration_controls_enabled:
            # if we are subscribed, we can always unregister
            if self.subscription_attached:
                button_sensitive = True
            # check if credentials are sufficient for registration
            elif self.authentication_method == AuthenticationMethod.USERNAME_PASSWORD:
                button_sensitive = username_password_sufficient(
                    self.subscription_request)
            elif self.authentication_method == AuthenticationMethod.ORG_KEY:
                button_sensitive = org_keys_sufficient(
                    self.subscription_request)
        self._register_button.set_sensitive(button_sensitive)
Пример #6
0
class SoftwareSelectionSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: SoftwareSelectionSpoke
          :parts: 3
    """
    builderObjects = ["addonStore", "environmentStore", "softwareWindow"]
    mainWidgetName = "softwareWindow"
    uiFile = "spokes/software_selection.glade"
    helpFile = "SoftwareSpoke.xml"

    category = SoftwareCategory

    icon = "package-x-generic-symbolic"
    title = CN_("GUI|Spoke", "_Software Selection")

    @classmethod
    def should_run(cls, environment, data):
        """Don't run for any non-package payload."""
        if not NormalSpoke.should_run(environment, data):
            return False

        return context.payload_type == PAYLOAD_TYPE_DNF

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._errors = []
        self._warnings = []
        self._tx_id = None

        # Get the packages selection data.
        self._selection_cache = SoftwareSelectionCache(self._dnf_manager)
        self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted

        # Get the UI elements.
        self._environment_list_box = self.builder.get_object("environmentListBox")
        self._addon_list_box = self.builder.get_object("addonListBox")

        # Connect viewport scrolling with listbox focus events
        environment_viewport = self.builder.get_object("environmentViewport")
        self._environment_list_box.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(environment_viewport)
        )

        addon_viewport = self.builder.get_object("addonViewport")
        self._addon_list_box.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(addon_viewport)
        )

    @property
    def _dnf_manager(self):
        """The DNF manager."""
        return self.payload.dnf_manager

    @property
    def _selection(self):
        """The packages selection."""
        return self.payload.get_packages_selection()

    def initialize(self):
        """Initialize the spoke."""
        super().initialize()
        self.initialize_start()

        threadMgr.add(AnacondaThread(
            name=THREAD_SOFTWARE_WATCHER,
            target=self._initialize
        ))

    def _initialize(self):
        """Initialize the spoke in a separate thread."""
        threadMgr.wait(THREAD_PAYLOAD)

        # Initialize and check the software selection.
        self._initialize_selection()

        # Update the status.
        hubQ.send_ready(self.__class__.__name__)

        # Report that the software spoke has been initialized.
        self.initialize_done()

    def _initialize_selection(self):
        """Initialize and check the software selection."""
        if not self.payload.base_repo:
            log.debug("Skip the initialization of the software selection.")
            return

        if not self._kickstarted:
            # Use the default environment.
            self._selection_cache.select_environment(
                self._dnf_manager.default_environment
            )

            # Apply the default selection.
            self.apply()

        # Check the initial software selection.
        self.execute()

        # Wait for the software selection thread that might be started by execute().
        # We are already running in a thread, so it should not needlessly block anything
        # and only like this we can be sure we are really initialized.
        threadMgr.wait(THREAD_CHECK_SOFTWARE)

    @property
    def ready(self):
        """Is the spoke ready?

        By default, the software selection spoke is not ready. We have to
        wait until the installation source spoke is completed. This could be
        because the user filled something out, or because we're done fetching
        repo metadata from the mirror list, or we detected a DVD/CD.
        """
        return not self._processing_data and self._source_is_set

    @property
    def _source_is_set(self):
        """Is the installation source set?"""
        return self.payload.base_repo is not None

    @property
    def _source_has_changed(self):
        """Has the installation source changed?"""
        return self._tx_id != self.payload.tx_id

    @property
    def _processing_data(self):
        """Is the spoke processing data?"""
        return threadMgr.get(THREAD_SOFTWARE_WATCHER) \
            or threadMgr.get(THREAD_PAYLOAD) \
            or threadMgr.get(THREAD_CHECK_SOFTWARE)

    @property
    def status(self):
        """The status of the spoke."""
        if self._processing_data:
            return _("Processing...")
        if is_cdn_registration_required(self.payload):
            return _("Red Hat CDN requires registration.")
        if not self._source_is_set:
            return _("Installation source not set up")
        if self._source_has_changed:
            return _("Source changed - please verify")
        if self._errors:
            return _("Error checking software selection")
        if self._warnings:
            return _("Warning checking software selection")

        return get_software_selection_status(
            dnf_manager=self._dnf_manager,
            selection=self._selection,
            kickstarted=self._kickstarted
        )

    @property
    def completed(self):
        """Is the spoke complete?"""
        return self.ready \
            and not self._errors \
            and not self._source_has_changed \
            and is_software_selection_complete(
                dnf_manager=self._dnf_manager,
                selection=self._selection,
                kickstarted=self._kickstarted
            )

    def refresh(self):
        super().refresh()
        threadMgr.wait(THREAD_PAYLOAD)

        # Create a new software selection cache.
        self._selection_cache = SoftwareSelectionCache(self._dnf_manager)
        self._selection_cache.apply_selection_data(self._selection)

        # Refresh up the UI.
        self._refresh_environments()
        self._refresh_groups()

        # Set up the info bar.
        self.clear_info()

        if self._errors:
            self.set_warning(_(
                "Error checking software dependencies. "
                " <a href=\"\">Click for details.</a>"
            ))
        elif self._warnings:
            self.set_warning(_(
                "Warning checking software dependencies. "
                " <a href=\"\">Click for details.</a>"
            ))

    def _refresh_environments(self):
        """Create rows for all available environments."""
        self._clear_listbox(self._environment_list_box)

        for environment in self._selection_cache.available_environments:
            # Get the environment data.
            data = self._dnf_manager.get_environment_data(environment)
            selected = self._selection_cache.is_environment_selected(environment)

            # Add a new environment row.
            row = EnvironmentListBoxRow(data, selected)
            self._environment_list_box.insert(row, -1)

        self._environment_list_box.show_all()

    def _refresh_groups(self):
        """Create rows for all available groups."""
        self._clear_listbox(self._addon_list_box)

        if self._selection_cache.environment:
            # Get the environment data.
            environment_data = self._dnf_manager.get_environment_data(
                self._selection_cache.environment
            )

            # Add all optional groups.
            for group in environment_data.optional_groups:
                self._add_group_row(group)

            # Add the separator.
            if environment_data.optional_groups and environment_data.visible_groups:
                self._addon_list_box.insert(SeparatorRow(), -1)

            # Add user visible groups that are not optional.
            for group in environment_data.visible_groups:
                if group in environment_data.optional_groups:
                    continue

                self._add_group_row(group)

        self._addon_list_box.show_all()

    def _add_group_row(self, group):
        """Add a new row for the specified group."""
        # Get the group data.
        data = self._dnf_manager.get_group_data(group)
        selected = self._selection_cache.is_group_selected(group)

        # Add a new group row.
        row = GroupListBoxRow(data, selected)
        self._addon_list_box.insert(row, -1)

    def _clear_listbox(self, listbox):
        for child in listbox.get_children():
            listbox.remove(child)
            del child

    def apply(self):
        """Apply the changes."""
        self._kickstarted = False

        selection = self._selection_cache.get_selection_data()
        log.debug("Setting new software selection: %s", selection)

        self.payload.set_packages_selection(selection)

        hubQ.send_not_ready(self.__class__.__name__)
        hubQ.send_not_ready("SourceSpoke")

    def execute(self):
        """Execute the changes."""
        threadMgr.add(AnacondaThread(
            name=THREAD_CHECK_SOFTWARE,
            target=self._check_software_selection
        ))

    def _check_software_selection(self):
        hubQ.send_message(self.__class__.__name__, _("Checking software dependencies..."))
        self.payload.bump_tx_id()

        # Run the validation task.
        from pyanaconda.modules.payloads.payload.dnf.validation import CheckPackagesSelectionTask

        task = CheckPackagesSelectionTask(
            dnf_manager=self._dnf_manager,
            selection=self._selection,
        )

        # Get the validation report.
        report = task.run()

        log.debug("The selection has been checked: %s", report)
        self._errors = list(report.error_messages)
        self._warnings = list(report.warning_messages)
        self._tx_id = self.payload.tx_id

        hubQ.send_ready(self.__class__.__name__)
        hubQ.send_ready("SourceSpoke")

    # Signal handlers
    def on_environment_activated(self, listbox, row):
        if not isinstance(row, EnvironmentListBoxRow):
            return

        # Mark the environment as selected.
        environment = row.get_environment_id()
        self._selection_cache.select_environment(environment)

        # Update the row button.
        row.toggle_button(True)

        # Update the screen.
        self._refresh_groups()

    def on_addon_activated(self, listbox, row):
        # Skip the separator.
        if not isinstance(row, GroupListBoxRow):
            return

        # Mark the group as selected or deselected.
        group = row.get_group_id()
        selected = not self._selection_cache.is_group_selected(group)

        if selected:
            self._selection_cache.select_group(row.data.id)
        else:
            self._selection_cache.deselect_group(row.data.id)

        # Update the row button.
        row.toggle_button(selected)

    def on_info_bar_clicked(self, *args):
        if self._errors:
            self._show_error_dialog()
        elif self._warnings:
            self._show_warning_dialog()

    def _show_error_dialog(self):
        label = _(
            "The software marked for installation has the following errors.  "
            "This is likely caused by an error with your installation source.  "
            "You can quit the installer, change your software source, or change "
            "your software selections."
        )

        buttons = [
            C_("GUI|Software Selection|Error Dialog", "_Quit"),
            C_("GUI|Software Selection|Error Dialog", "_Modify Software Source"),
            C_("GUI|Software Selection|Error Dialog", "Modify _Selections")
        ]

        dialog = DetailedErrorDialog(self.data, buttons=buttons, label=label)

        with self.main_window.enlightbox(dialog.window):
            errors = "\n".join(self._errors)
            dialog.refresh(errors)
            rc = dialog.run()

        dialog.window.destroy()

        if rc == 0:
            # Quit the installation.
            ipmi_abort(scripts=self.data.scripts)
            sys.exit(0)
        elif rc == 1:
            # Send the user to the installation source spoke.
            self.skipTo = "SourceSpoke"
            self.window.emit("button-clicked")

    def _show_warning_dialog(self):
        label = _(
            "The software marked for installation has the following warnings. "
            "These are not fatal, but you may wish to make changes to your "
            "software selections."
        )

        buttons = [
            C_("GUI|Software Selection|Warning Dialog", "_OK")
        ]

        dialog = DetailedErrorDialog(self.data, buttons=buttons, label=label)

        with self.main_window.enlightbox(dialog.window):
            warnings = "\n".join(self._warnings)
            dialog.refresh(warnings)
            dialog.run()

        dialog.window.destroy()
Пример #7
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)

        self._users_module = USERS.get_observer()
        self._users_module.connect()

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()

        # Create a new UserData object to store this spoke's state
        # as well as the state of the advanced user dialog.
        if self.data.user.userList:
            self._user = copy.copy(self.data.user.userList[0])
        else:
            self._user = self.data.UserData()

        # gather references to relevant GUI objects

        # entry fields
        self._fullname_entry = self.builder.get_object("fullname_entry")
        self._username_entry = self.builder.get_object("username_entry")
        self._password_entry = self.builder.get_object("password_entry")
        self._password_confirmation_entry = self.builder.get_object(
            "password_confirmation_entry")
        # check boxes
        self._admin_checkbox = self.builder.get_object("admin_checkbox")
        self._password_required_checkbox = self.builder.get_object(
            "password_required_checkbox")
        # advanced user configration dialog button
        self._advanced_button = self.builder.get_object("advanced_button")
        # password checking status bar & label
        self._password_bar = self.builder.get_object("password_bar")
        self._password_label = self.builder.get_object("password_label")

        # Install the password checks:
        # - Has a password been specified?
        # - If a password has been specified and there is data in the confirm box, do they match?
        # - How strong is the password?
        # - Does the password contain non-ASCII characters?

        # Setup the password checker for password checking
        self._checker = input_checking.PasswordChecker(
            initial_password_content=self.password,
            initial_password_confirmation_content=self.password_confirmation,
            policy=input_checking.get_policy(self.data, "user"))
        # configure the checker for password checking
        self.checker.username = self.username
        self.checker.secret_type = constants.SecretType.PASSWORD
        # remove any placeholder texts if either password or confirmation field changes content from initial state
        self.checker.password.changed_from_initial_state.connect(
            self.remove_placeholder_texts)
        self.checker.password_confirmation.changed_from_initial_state.connect(
            self.remove_placeholder_texts)
        # connect UI updates to check results
        self.checker.checks_done.connect(self._checks_done)

        # username and full name checks
        self._username_check = input_checking.UsernameCheck()
        self._fullname_check = input_checking.FullnameCheck()
        # empty username is considered a success so that the user can leave
        # the spoke without filling it in
        self._username_check.success_if_username_empty = True
        # check that the password is not empty
        self._empty_check = input_checking.PasswordEmptyCheck()
        # check that the content of the password field & the conformation field are the same
        self._confirm_check = input_checking.PasswordConfirmationCheck()
        # check password validity, quality and strength
        self._validity_check = input_checking.PasswordValidityCheck()
        # connect UI updates to validity check results
        self._validity_check.result.password_score_changed.connect(
            self.set_password_score)
        self._validity_check.result.status_text_changed.connect(
            self.set_password_status)
        # check if the password contains non-ascii characters
        self._ascii_check = input_checking.PasswordASCIICheck()

        # register the individual checks with the checker in proper order
        # 0) is the username and fullname valid ?
        # 1) is the password non-empty ?
        # 2) are both entered passwords the same ?
        # 3) is the password valid according to the current password checking policy ?
        # 4) is the password free of non-ASCII characters ?
        self.checker.add_check(self._username_check)
        self.checker.add_check(self._fullname_check)
        self.checker.add_check(self._empty_check)
        self.checker.add_check(self._confirm_check)
        self.checker.add_check(self._validity_check)
        self.checker.add_check(self._ascii_check)

        self.guesser = {self.username_entry: True}

        # Configure levels for the password bar
        self.password_bar.add_offset_value("low", 2)
        self.password_bar.add_offset_value("medium", 3)
        self.password_bar.add_offset_value("high", 4)

        # indicate when the password was set by kickstart
        self.password_kickstarted = self.data.user.seen

        # Modify the GUI based on the kickstart and policy information
        # This needs to happen after the input checks have been created, since
        # the Gtk signal handlers use the input check variables.
        password_set_message = _("The password was set by kickstart.")
        if self.password_kickstarted:
            self.password_required = True
            self.password_entry.set_placeholder_text(password_set_message)
            self.password_confirmation_entry.set_placeholder_text(
                password_set_message)
        elif not self.checker.policy.emptyok:
            # Policy is that a non-empty password is required
            self.password_required = True

        if not self.checker.policy.emptyok:
            # User isn't allowed to change whether password is required or not
            self.password_required_checkbox.set_sensitive(False)

        self._advanced_user_dialog = AdvancedUserDialog(self._user, self.data)
        self._advanced_user_dialog.initialize()

        # set the visibility of the password entries
        set_password_visibility(self.password_entry, False)
        set_password_visibility(self.password_confirmation_entry, False)

        # report that we are done
        self.initialize_done()

    @property
    def username_entry(self):
        return self._username_entry

    @property
    def username(self):
        return self.username_entry.get_text()

    @username.setter
    def username(self, new_username):
        self.username_entry.set_text(new_username)

    @property
    def fullname_entry(self):
        return self._fullname_entry

    @property
    def fullname(self):
        return self.fullname_entry.get_text()

    @fullname.setter
    def fullname(self, new_fullname):
        self.fullname_entry.set_text(new_fullname)

    @property
    def password_required_checkbox(self):
        return self._password_required_checkbox

    @property
    def password_required(self):
        return self.password_required_checkbox.get_active()

    @password_required.setter
    def password_required(self, value):
        self.password_required_checkbox.set_active(value)

    def refresh(self):
        self.username = self._user.name
        self.fullname = self._user.gecos
        self._admin_checkbox.set_active("wheel" in self._user.groups)

        # rerun checks so that we have a correct status message, if any
        self.checker.run_checks()

    @property
    def status(self):
        if len(self.data.user.userList) == 0:
            return _("No user will be created")
        elif "wheel" in self.data.user.userList[0].groups:
            return _("Administrator %s will be created"
                     ) % self.data.user.userList[0].name
        else:
            return _(
                "User %s will be created") % self.data.user.userList[0].name

    @property
    def mandatory(self):
        """ Only mandatory if the root pw hasn't been set in the UI
            eg. not mandatory if the root account was locked in a kickstart
        """
        return not self._users_module.proxy.IsRootPasswordSet and not self._users_module.proxy.IsRootAccountLocked

    def apply(self):
        # set the password only if the user enters anything to the text entry
        # this should preserve the kickstart based password
        if self.password_required:
            if self.password:
                self.password_kickstarted = False
                self._user.password = cryptPassword(self.password)
                self._user.isCrypted = True
                self.remove_placeholder_texts()

        # reset the password when the user unselects it
        else:
            self.remove_placeholder_texts()
            self._user.password = ""
            self._user.isCrypted = False
            self.password_kickstarted = False

        self._user.name = self.username
        self._user.gecos = self.fullname

        # Copy the spoke data back to kickstart
        # If the user name is not set, no user will be created.
        if self._user.name:
            ksuser = copy.copy(self._user)
            if not self.data.user.userList:
                self.data.user.userList.append(ksuser)
            else:
                self.data.user.userList[0] = ksuser
        elif self.data.user.userList:
            self.data.user.userList.pop(0)

    @property
    def sensitive(self):
        # Spoke cannot be entered if a user was set in the kickstart and the user
        # policy doesn't allow changes.
        return not (self.completed and flags.automatedInstall and
                    self.data.user.seen and not self.checker.policy.changesok)

    @property
    def completed(self):
        return len(self.data.user.userList) > 0

    def password_required_toggled(self, togglebutton=None, data=None):
        """Called by Gtk callback when the "Use password" check
        button is toggled. It will make password entries in/sensitive."""
        password_is_required = togglebutton.get_active()
        self.password_entry.set_sensitive(password_is_required)
        self.password_confirmation_entry.set_sensitive(password_is_required)

        # also disable/enable corresponding password checks
        self._empty_check.skip = not password_is_required
        self._confirm_check.skip = not password_is_required
        self._validity_check.skip = not password_is_required
        self._ascii_check.skip = not password_is_required

        # and rerun the checks
        self.checker.run_checks()

    def on_password_icon_clicked(self, entry, icon_pos, event):
        """Called by Gtk callback when the icon of a password entry is clicked."""
        set_password_visibility(entry, not entry.get_visibility())

    def on_username_set_by_user(self, editable, data=None):
        """Called by Gtk on user-driven changes to the username field.

           This handler is blocked during changes from the username guesser.
        """

        # If the user set a user name, turn off the username guesser.
        # If the user cleared the username, turn it back on.
        if editable.get_text():
            self.guesser = False
        else:
            self.guesser = True

    def on_username_changed(self, editable, data=None):
        """Called by Gtk on all username changes."""
        new_username = editable.get_text()

        # Disable the advanced user dialog button when no username is set
        if editable.get_text():
            self._advanced_button.set_sensitive(True)
        else:
            self._advanced_button.set_sensitive(False)

        # update the username in checker
        self.checker.username = new_username

        # Skip the empty password checks if no username is set,
        # otherwise the user will not be able to leave the
        # spoke if password is not set but policy requires that.
        self._empty_check.skip = not new_username
        self._validity_check.skip = not new_username
        # Re-run the password checks against the new username
        self.checker.run_checks()

    def on_full_name_changed(self, editable, data=None):
        """Called by Gtk callback when the full name field changes."""

        fullname = editable.get_text()
        if self.guesser:
            username = guess_username(fullname)
            with blockedHandler(self.username_entry,
                                self.on_username_set_by_user):
                self.username = username

        self.checker.fullname = fullname

        # rerun the checks
        self.checker.run_checks()

    def on_admin_toggled(self, togglebutton, data=None):
        # Add or remove "wheel" from the grouplist on changes to the admin checkbox
        if togglebutton.get_active():
            if "wheel" not in self._user.groups:
                self._user.groups.append("wheel")
        elif "wheel" in self._user.groups:
            self._user.groups.remove("wheel")

    def on_advanced_clicked(self, _button, data=None):
        """Handler for the Advanced.. button. It starts the Advanced dialog
        for setting homedir, uid, gid and groups.
        """

        self._user.name = self.username

        self._advanced_user_dialog.refresh()
        with self.main_window.enlightbox(self._advanced_user_dialog.window):
            self._advanced_user_dialog.run()

        self._admin_checkbox.set_active("wheel" in self._user.groups)

    def _checks_done(self, error_message):
        """Update the warning with the input validation error from the first
           error message or clear warnings if all the checks were successful.

           Also appends the "press twice" suffix if compatible with current
           password policy and handles the press-done-twice logic.
        """

        # check if an unwaivable check failed
        unwaivable_checks = [
            not self._confirm_check.result.success,
            not self._username_check.result.success,
            not self._fullname_check.result.success,
            not self._empty_check.result.success
        ]
        # with emptyok == False the empty password check become unwaivable
        #if not self.checker.policy.emptyok:
        #    unwaivable_checks.append(not self._empty_check.result.success)
        unwaivable_check_failed = any(unwaivable_checks)

        # set appropriate status bar message
        if not error_message:
            # all is fine, just clear the message
            self.clear_info()
        elif not self.username and not self.password and not self.password_confirmation:
            # Clear any info message if username and both the password and password
            # confirmation fields are empty.
            # This shortcut is done to make it possible for the user to leave the spoke
            # without inputting any username or password. Separate logic makes sure an
            # empty string is not unexpectedly set as the user password.
            self.clear_info()
        elif not self.username and not self.password and not self.password_confirmation:
            # Also clear warnings if username is set but empty password is fine.
            self.clear_info()
        else:
            if self.checker.policy.strict or unwaivable_check_failed:
                # just forward the error message
                self.show_warning_message(error_message)
            else:
                # add suffix for the click twice logic
                self.show_warning_message("{} {}".format(
                    error_message, _(constants.PASSWORD_DONE_TWICE)))

        # check if the spoke can be exited after the latest round of checks
        self._check_spoke_exit_conditions(unwaivable_check_failed)

    def _check_spoke_exit_conditions(self, unwaivable_check_failed):
        """Check if the user can escape from the root spoke or stay forever !"""

        # reset any waiving in progress
        self.waive_clicks = 0

        # Depending on the policy we allow users to waive the password strength
        # and non-ASCII checks. If the policy is set to strict, the password
        # needs to be strong, but can still contain non-ASCII characters.
        self.can_go_back = False
        self.needs_waiver = True

        # This shortcut is done to make it possible for the user to leave the spoke
        # without inputting anything. Separate logic makes sure an
        # empty string is not unexpectedly set as the user password.
        if not self.username and not self.password and not self.password_confirmation:
            self.can_go_back = True
            self.needs_waiver = False
        elif self.checker.success:
            # if all checks were successful we can always go back to the hub
            self.can_go_back = True
            self.needs_waiver = False
        elif unwaivable_check_failed:
            self.can_go_back = False
        elif not self.password and not self.password_confirmation:
            self.can_go_back = True
            self.needs_waiver = False
        else:
            if self.checker.policy.strict:
                if not self._validity_check.result.success:
                    # failing validity check in strict
                    # mode prevents us from going back
                    self.can_go_back = False
                elif not self._ascii_check.result.success:
                    # but the ASCII check can still be waived
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False
            else:
                if not self._confirm_check.result.success:
                    self.can_go_back = False
                if not self._validity_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                elif not self._ascii_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False

    def on_back_clicked(self, button):
        # the GUI spoke input check handler handles the spoke exit logic for us
        if self.try_to_go_back():
            NormalSpoke.on_back_clicked(self, button)
        else:
            log.info("Return to hub prevented by password checking rules.")
Пример #8
0
class StorageSpoke(NormalSpoke, StorageCheckHandler):
    """
       .. inheritance-diagram:: StorageSpoke
          :parts: 3
    """
    builderObjects = ["storageWindow", "addSpecializedImage"]
    mainWidgetName = "storageWindow"
    uiFile = "spokes/storage.glade"
    category = SystemCategory
    # other candidates: computer-symbolic, folder-symbolic
    icon = "drive-harddisk-symbolic"
    title = CN_("GUI|Spoke", "Installation _Destination")

    @staticmethod
    def get_screen_id():
        """Return a unique id of this UI screen."""
        return "storage-configuration"

    @classmethod
    def should_run(cls, environment, data):
        """Don't run the storage spoke on dir installations."""
        if not NormalSpoke.should_run(environment, data):
            return False

        return not conf.target.is_directory

    def __init__(self, *args, **kwargs):
        StorageCheckHandler.__init__(self)
        NormalSpoke.__init__(self, *args, **kwargs)
        self.applyOnSkip = True
        self._ready = False
        self._back_clicked = False
        self._disks_errors = []
        self._last_clicked_overview = None
        self._cur_clicked_overview = None

        self._storage_module = STORAGE.get_proxy()
        self._device_tree = STORAGE.get_proxy(DEVICE_TREE)
        self._bootloader_module = STORAGE.get_proxy(BOOTLOADER)
        self._disk_init_module = STORAGE.get_proxy(DISK_INITIALIZATION)
        self._disk_select_module = STORAGE.get_proxy(DISK_SELECTION)

        # This list contains all possible disks that can be included in the install.
        # All types of advanced disks should be set up for us ahead of time, so
        # there should be no need to modify this list.
        self._available_disks = []
        self._selected_disks = []
        self._last_selected_disks = []

        # Is the partitioning already configured?
        self._is_preconfigured = bool(self._storage_module.CreatedPartitioning)

        # Find a partitioning to use.
        self._partitioning = find_partitioning()
        self._last_partitioning_method = self._partitioning.PartitioningMethod

        # Create a partitioning request for the automatic partitioning.
        self._partitioning_request = PartitioningRequest()

        if self._last_partitioning_method == PARTITIONING_METHOD_AUTOMATIC:
            self._partitioning_request = PartitioningRequest.from_structure(
                self._partitioning.Request)

        # Get the UI elements.
        self._custom_part_radio_button = self.builder.get_object(
            "customRadioButton")
        self._blivet_gui_radio_button = self.builder.get_object(
            "blivetguiRadioButton")
        self._encrypted_checkbox = self.builder.get_object(
            "encryptionCheckbox")
        self._encryption_revealer = self.builder.get_object(
            "encryption_revealer")
        self._reclaim_checkbox = self.builder.get_object("reclaimCheckbox")
        self._reclaim_revealer = self.builder.get_object(
            "reclaim_checkbox_revealer")
        self._local_disks_box = self.builder.get_object("local_disks_box")
        self._specialized_disks_box = self.builder.get_object(
            "specialized_disks_box")
        self._local_viewport = self.builder.get_object("localViewport")
        self._specialized_viewport = self.builder.get_object(
            "specializedViewport")
        self._main_viewport = self.builder.get_object("storageViewport")
        self._main_box = self.builder.get_object("storageMainBox")

        # Configure the partitioning methods.
        self._configure_partitioning_methods()

    def _configure_partitioning_methods(self):
        if "CustomPartitioningSpoke" in conf.ui.hidden_spokes:
            self._custom_part_radio_button.set_visible(False)
            self._custom_part_radio_button.set_no_show_all(True)

        if "BlivetGuiSpoke" in conf.ui.hidden_spokes or not self._is_blivet_gui_supported(
        ):
            self._blivet_gui_radio_button.set_visible(False)
            self._blivet_gui_radio_button.set_no_show_all(True)

    def _is_blivet_gui_supported(self):
        """Is the partitioning with blivet-gui supported?"""
        try:
            import pyanaconda.ui.gui.spokes.blivet_gui  # pylint:disable=unused-import
        except ImportError:
            return False

        return True

    def _get_selected_partitioning_method(self):
        """Get the selected partitioning method.

        Return partitioning method according to which method selection
        radio button is currently active.
        """
        if self._custom_part_radio_button.get_active():
            return PARTITIONING_METHOD_INTERACTIVE

        if self._blivet_gui_radio_button.get_active():
            return PARTITIONING_METHOD_BLIVET

        return PARTITIONING_METHOD_AUTOMATIC

    def on_method_toggled(self, radio_button):
        """Triggered when one of the partitioning method radio buttons is toggled."""
        # Run only for a visible active radio button.
        if not radio_button.get_visible() or not radio_button.get_active():
            return

        # Get the selected patitioning method.
        current_partitioning_method = self._get_selected_partitioning_method()

        # Hide the encryption checkbox for Blivet GUI storage configuration,
        # as Blivet GUI handles encryption per encrypted device, not globally.
        # Hide it also for the interactive partitioning as CustomPartitioningSpoke
        # provides support for encryption of mount points.
        self._encryption_revealer.set_reveal_child(
            current_partitioning_method == PARTITIONING_METHOD_AUTOMATIC)

        # Hide the reclaim space checkbox if automatic storage configuration is not used.
        self._reclaim_revealer.set_reveal_child(
            current_partitioning_method == PARTITIONING_METHOD_AUTOMATIC)

        # Is this a change from the last used method ?
        method_changed = current_partitioning_method != self._last_partitioning_method

        # Are there any actions planned ?
        if self._storage_module.AppliedPartitioning:
            if method_changed:
                # clear any existing messages from the info bar
                # - this generally means various storage related error warnings
                self.clear_info()
                self.set_warning(
                    _("Partitioning method changed - planned storage configuration "
                      "changes will be cancelled."))
            else:
                self.clear_info()
                # reinstate any errors that should be shown to the user
                self._check_problems()

    def apply(self):
        self._disk_init_module.InitializationMode = CLEAR_PARTITIONS_NONE
        self._disk_init_module.InitializeLabelsEnabled = True
        apply_disk_selection(self._selected_disks, reset_boot_drive=True)

    @async_action_nowait
    def execute(self):
        """Apply a partitioning."""
        # Make sure that we apply a non-interactive partitioning.
        if self._last_partitioning_method == PARTITIONING_METHOD_INTERACTIVE:
            log.debug(
                "Skipping the execute method for the INTERACTIVE partitioning method."
            )
            return

        if self._last_partitioning_method == PARTITIONING_METHOD_BLIVET:
            log.debug(
                "Skipping the execute method for the BLIVET partitioning method."
            )
            return

        log.debug("Running the execute method for the %s partitioning method.",
                  self._last_partitioning_method)

        # Spawn storage execution as a separate thread so there's no big delay
        # going back from this spoke to the hub while StorageCheckHandler.run runs.
        # Yes, this means there's a thread spawning another thread.  Sorry.
        threadMgr.add(
            AnacondaThread(name=constants.THREAD_EXECUTE_STORAGE,
                           target=self._do_execute))

    def _do_execute(self):
        """Apply a non-interactive partitioning."""
        self._ready = False
        hubQ.send_not_ready(self.__class__.__name__)

        report = apply_partitioning(partitioning=self._partitioning,
                                    show_message_cb=self._show_execute_message,
                                    reset_storage_cb=self._reset_storage)

        StorageCheckHandler.errors = list(report.error_messages)
        StorageCheckHandler.warnings = list(report.warning_messages)

        self._ready = True
        hubQ.send_ready(self.__class__.__name__)

    def _show_execute_message(self, msg):
        hubQ.send_message(self.__class__.__name__, msg)
        log.debug(msg)

    def _reset_storage(self):
        reset_storage(scan_all=True)

    @property
    def completed(self):
        return self.ready and not self.errors and self._device_tree.GetRootDevice(
        )

    @property
    def ready(self):
        # By default, the storage spoke is not ready.  We have to wait until
        # storageInitialize is done.
        return self._ready \
            and not threadMgr.get(constants.THREAD_STORAGE) \
            and not threadMgr.get(constants.THREAD_DASDFMT) \
            and not threadMgr.get(constants.THREAD_EXECUTE_STORAGE)

    @property
    def status(self):
        """ A short string describing the current status of storage setup. """
        if not self.ready:
            return _("Processing...")
        elif flags.automatedInstall and not self._device_tree.GetRootDevice():
            return _("Kickstart insufficient")
        elif not self._disk_select_module.SelectedDisks:
            return _("No disks selected")
        elif self.errors:
            return _("Error checking storage configuration")
        elif self.warnings:
            return _("Warning checking storage configuration")
        elif self._last_partitioning_method == PARTITIONING_METHOD_AUTOMATIC:
            return _("Automatic partitioning selected")
        else:
            return _("Custom partitioning selected")

    @property
    def local_overviews(self):
        return self._local_disks_box.get_children()

    @property
    def advanced_overviews(self):
        return [
            child for child in self._specialized_disks_box.get_children()
            if isinstance(child, AnacondaWidgets.DiskOverview)
        ]

    def _on_disk_clicked(self, overview, event):
        # This handler only runs for these two kinds of events, and only for
        # activate-type keys (space, enter) in the latter event's case.
        if event.type not in [
                Gdk.EventType.BUTTON_PRESS, Gdk.EventType.KEY_RELEASE
        ]:
            return

        if event.type == Gdk.EventType.KEY_RELEASE and \
           event.keyval not in [Gdk.KEY_space, Gdk.KEY_Return, Gdk.KEY_ISO_Enter,
                                Gdk.KEY_KP_Enter, Gdk.KEY_KP_Space]:
            return

        if event.type == Gdk.EventType.BUTTON_PRESS and \
                event.state & Gdk.ModifierType.SHIFT_MASK:
            # clicked with Shift held down

            if self._last_clicked_overview is None:
                # nothing clicked before, cannot apply Shift-click
                return

            local_overviews = self.local_overviews
            advanced_overviews = self.advanced_overviews

            # find out which list of overviews the clicked one belongs to
            if overview in local_overviews:
                from_overviews = local_overviews
            elif overview in advanced_overviews:
                from_overviews = advanced_overviews
            else:
                # should never happen, but if it does, no other actions should be done
                return

            if self._last_clicked_overview in from_overviews:
                # get index of the last clicked overview
                last_idx = from_overviews.index(self._last_clicked_overview)
            else:
                # overview from the other list clicked before, cannot apply "Shift-click"
                return

            # get index and state of the clicked overview
            cur_idx = from_overviews.index(overview)
            state = self._last_clicked_overview.get_chosen()

            if cur_idx > last_idx:
                copy_to = from_overviews[last_idx:cur_idx + 1]
            else:
                copy_to = from_overviews[cur_idx:last_idx]

            # copy the state of the last clicked overview to the ones between it and the
            # one clicked with the Shift held down
            for disk_overview in copy_to:
                disk_overview.set_chosen(state)

        self._update_disk_list()
        self._update_summary()

    def _on_disk_focus_in(self, overview, event):
        self._last_clicked_overview = self._cur_clicked_overview
        self._cur_clicked_overview = overview

    def refresh(self):
        self._back_clicked = False

        self._available_disks = self._disk_select_module.GetUsableDisks()
        self._selected_disks = self._disk_select_module.SelectedDisks

        # Get the available selected disks.
        self._selected_disks = filter_disks_by_names(self._available_disks,
                                                     self._selected_disks)

        # First, remove all non-button children.
        for child in self.local_overviews + self.advanced_overviews:
            child.destroy()

        # Then deal with local disks, which are really easy.  They need to be
        # handled here instead of refresh to take into account the user pressing
        # the rescan button on custom partitioning.
        # Advanced disks are different.  Because there can potentially be a lot
        # of them, we do not display them in the box by default.  Instead, only
        # those selected in the filter UI are displayed.  This means refresh
        # needs to know to create and destroy overviews as appropriate.
        for device_name in self._available_disks:

            # Get the device data.
            device_data = DeviceData.from_structure(
                self._device_tree.GetDeviceData(device_name))

            if is_local_disk(device_data.type):
                # Add all available local disks.
                self._add_disk_overview(device_data, self._local_disks_box)

            elif device_name in self._selected_disks:
                # Add only selected advanced disks.
                self._add_disk_overview(device_data,
                                        self._specialized_disks_box)

        # update the selections in the ui
        for overview in self.local_overviews + self.advanced_overviews:
            name = overview.get_property("name")
            overview.set_chosen(name in self._selected_disks)

        # Update the encryption checkbox.
        if self._partitioning_request.encrypted:
            self._encrypted_checkbox.set_active(True)

        self._update_summary()
        self._check_problems()

    def _check_problems(self):
        if self.errors:
            self.set_warning(
                _("Error checking storage configuration.  "
                  "<a href=\"\">Click for details.</a>"))
            return True
        elif self.warnings:
            self.set_warning(
                _("Warning checking storage configuration.  "
                  "<a href=\"\">Click for details.</a>"))
            return True
        return False

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()

        # Connect the viewport adjustments to the child widgets
        # See also https://bugzilla.gnome.org/show_bug.cgi?id=744721
        self._local_disks_box.set_focus_hadjustment(
            Gtk.Scrollable.get_hadjustment(self._local_viewport))

        self._specialized_disks_box.set_focus_hadjustment(
            Gtk.Scrollable.get_hadjustment(self._specialized_viewport))

        self._main_box.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(self._main_viewport))

        threadMgr.add(
            AnacondaThread(name=constants.THREAD_STORAGE_WATCHER,
                           target=self._initialize))

    def _add_disk_overview(self, device_data, box):
        if device_data.type == "dm-multipath":
            # We don't want to display the whole huge WWID for a multipath device.
            wwn = device_data.attrs.get("wwn", "")
            description = wwn[0:6] + "..." + wwn[-8:]
        elif device_data.type == "zfcp":
            # Manually mangle the desc of a zFCP device to be multi-line since
            # it's so long it makes the disk selection screen look odd.
            description = _(
                "FCP device {hba_id}\nWWPN {wwpn}\nLUN {lun}").format(
                    hba_id=device_data.attrs.get("hba-id", ""),
                    wwpn=device_data.attrs.get("wwpn", ""),
                    lun=device_data.attrs.get("fcp-lun", ""))
        elif device_data.type == "nvdimm":
            description = _("NVDIMM device {namespace}").format(
                namespace=device_data.attrs.get("namespace", ""))
        else:
            description = device_data.description

        kind = "drive-removable-media" if device_data.removable else "drive-harddisk"
        free_space = self._device_tree.GetDiskFreeSpace([device_data.name])
        serial_number = device_data.attrs.get("serial") or None

        overview = AnacondaWidgets.DiskOverview(
            description, kind, str(Size(device_data.size)),
            _("{} free").format(str(Size(free_space))), device_data.name,
            serial_number)

        box.pack_start(overview, False, False, 0)
        overview.set_chosen(device_data.name in self._selected_disks)
        overview.connect("button-press-event", self._on_disk_clicked)
        overview.connect("key-release-event", self._on_disk_clicked)
        overview.connect("focus-in-event", self._on_disk_focus_in)
        overview.show_all()

    def _initialize(self):
        """Finish the initialization.

        This method is expected to run only once during the initialization.
        """
        # Wait for storage.
        hubQ.send_message(self.__class__.__name__,
                          _(constants.PAYLOAD_STATUS_PROBING_STORAGE))
        threadMgr.wait(constants.THREAD_STORAGE)

        # Automatically format DASDs if allowed.
        disks = self._disk_select_module.GetUsableDisks()
        DasdFormatting.run_automatically(disks, self._show_dasdfmt_report)
        hubQ.send_message(self.__class__.__name__,
                          _(constants.PAYLOAD_STATUS_PROBING_STORAGE))

        # Update the selected disks.
        select_default_disks()

        # Automatically apply the preconfigured partitioning.
        # Do not set ready in the automated installation before
        # the execute method is run.
        if flags.automatedInstall and self._is_preconfigured:
            self._check_required_passphrase()
            self.execute()
        else:
            self._ready = True
            hubQ.send_ready(self.__class__.__name__)

        # Report that the storage spoke has been initialized.
        self.initialize_done()

    def _show_dasdfmt_report(self, msg):
        hubQ.send_message(self.__class__.__name__, msg)

    @async_action_wait
    def _check_required_passphrase(self):
        """Ask a user for a default passphrase if required."""
        if not is_passphrase_required(self._partitioning):
            return

        dialog = PassphraseDialog(self.data)

        with self.main_window.enlightbox(dialog.window):
            rc = dialog.run()

        if rc != 1:
            return

        set_required_passphrase(self._partitioning, dialog.passphrase)

    def _update_summary(self):
        """ Update the summary based on the UI. """
        disks = filter_disks_by_names(self._available_disks,
                                      self._selected_disks)
        summary = get_disks_summary(disks)

        summary_label = self.builder.get_object("summary_label")
        summary_label.set_text(summary)

        is_selected = bool(self._selected_disks)
        summary_label.set_sensitive(is_selected)

        # only show the "we won't touch your other disks" labels and summary button when
        # some disks are selected
        self.builder.get_object("summary_button_revealer").set_reveal_child(
            is_selected)
        self.builder.get_object(
            "local_untouched_label_revealer").set_reveal_child(is_selected)
        self.builder.get_object(
            "special_untouched_label_revealer").set_reveal_child(is_selected)
        self.builder.get_object("other_options_grid").set_sensitive(
            is_selected)

        if not self._available_disks:
            self.set_warning(_(WARNING_NO_DISKS_DETECTED))
        elif not self._selected_disks:
            # There may be an underlying reason that no disks were selected, give them priority.
            if not self._check_problems():
                self.set_warning(_(WARNING_NO_DISKS_SELECTED))
        else:
            self.clear_info()

    def _update_disk_list(self):
        """ Update self.selected_disks based on the UI. """
        for overview in self.local_overviews + self.advanced_overviews:
            selected = overview.get_chosen()
            name = overview.get_property("name")

            if selected and name not in self._selected_disks:
                self._selected_disks.append(name)

            if not selected and name in self._selected_disks:
                self._selected_disks.remove(name)

    # signal handlers
    def on_summary_clicked(self, button):
        # show the selected disks dialog
        disks = filter_disks_by_names(self._available_disks,
                                      self._selected_disks)
        dialog = SelectedDisksDialog(self.data, disks)
        dialog.refresh()

        self.run_lightbox_dialog(dialog)

        # update selected disks since some may have been removed
        self._selected_disks = list(dialog.disks)

        # update the UI to reflect changes to self.selected_disks
        for overview in self.local_overviews + self.advanced_overviews:
            name = overview.get_property("name")
            overview.set_chosen(name in self._selected_disks)

        self._update_summary()

        if self._bootloader_module.BootloaderMode != BOOTLOADER_ENABLED:
            self.set_warning(
                _("You have chosen to skip boot loader installation. "
                  "Your system may not be bootable."))
        else:
            self.clear_info()

    def run_lightbox_dialog(self, dialog):
        with self.main_window.enlightbox(dialog.window):
            rc = dialog.run()

        return rc

    def _check_dasd_formats(self):
        # No change by default.
        rc = DASD_FORMAT_NO_CHANGE

        # Do nothing if unsupported.
        if not DasdFormatting.is_supported():
            return rc

        # Allow to format DASDs.
        self._disk_init_module.FormatUnrecognizedEnabled = True
        self._disk_init_module.FormatLDLEnabled = True

        # Get selected disks.
        disks = filter_disks_by_names(self._available_disks,
                                      self._selected_disks)

        # Check if some of the disks should be formatted.
        dasd_formatting = DasdFormatting()
        dasd_formatting.search_disks(disks)

        if dasd_formatting.should_run():
            # We want to apply current selection before running dasdfmt to
            # prevent this information from being lost afterward
            apply_disk_selection(self._selected_disks)

            # Run the dialog.
            dialog = DasdFormatDialog(self.data, dasd_formatting)
            ignoreEscape(dialog.window)
            rc = self.run_lightbox_dialog(dialog)

        return rc

    def _check_space_and_run_dialog(self, partitioning, disks):
        # User wants to reclaim the space.
        if self._reclaim_checkbox.get_active():
            return RESPONSE_RECLAIM

        # Get the device tree of the partitioning module.
        device_tree = STORAGE.get_proxy(partitioning.GetDeviceTree())

        # Calculate the required and free space.
        disk_free = Size(device_tree.GetDiskFreeSpace(disks))
        fs_free = Size(device_tree.GetDiskReclaimableSpace(disks))
        disks_size = Size(device_tree.GetDiskTotalSpace(disks))
        sw_space = Size(self.payload.space_required)
        auto_swap = suggest_swap_size()

        log.debug("disk free: %s  fs free: %s  sw needs: %s  auto swap: %s",
                  disk_free, fs_free, sw_space, auto_swap)

        # We need enough space for the software, the swap and the metadata.
        # It is not an ideal estimate, but it works.
        required_space = sw_space + auto_swap + STORAGE_METADATA_RATIO * disk_free

        # There is enough space to continue.
        if disk_free >= required_space:
            return RESPONSE_OK

        # Ask user what to do.
        if disks_size >= required_space - auto_swap:
            dialog = NeedSpaceDialog(self.data, payload=self.payload)
            dialog.refresh(required_space, sw_space, auto_swap, disk_free,
                           fs_free)
        else:
            dialog = NoSpaceDialog(self.data, payload=self.payload)
            dialog.refresh(required_space, sw_space, auto_swap, disk_free,
                           fs_free)

        return self.run_lightbox_dialog(dialog)

    def on_back_clicked(self, button):
        if self._back_clicked:
            return

        # Skip if user is clicking multiple times on the back button.
        self._back_clicked = True

        # Clear the current warning message if any.
        self.clear_info()

        # No disks selected?  The user wants to back out of the storage spoke.
        if not self._selected_disks:
            NormalSpoke.on_back_clicked(self, button)
            return

        # Reset to a snapshot if necessary.
        self._reset_to_snapshot()

        # The disk selection has to make sense before we can proceed.
        if not self._check_disk_selection():
            self._back_clicked = False
            return

        # Check for unsupported DASDs.
        rc = self._check_dasd_formats()
        if rc == DASD_FORMAT_NO_CHANGE:
            pass
        elif rc == DASD_FORMAT_REFRESH:
            # User hit OK on the dialog
            self.refresh()
        elif rc == DASD_FORMAT_RETURN_TO_HUB:
            # User clicked uri to return to hub.
            NormalSpoke.on_back_clicked(self, button)
            return
        else:
            # User either hit cancel on the dialog or closed
            # it via escape, there was no formatting done.
            self._back_clicked = False
            return

        # Handle the partitioning.
        partitioning_method = self._get_selected_partitioning_method()
        self._last_partitioning_method = partitioning_method

        if partitioning_method == PARTITIONING_METHOD_AUTOMATIC:
            self._skip_to_automatic_partitioning()
            return

        if partitioning_method == PARTITIONING_METHOD_INTERACTIVE:
            self._skip_to_spoke("CustomPartitioningSpoke")
            return

        if partitioning_method == PARTITIONING_METHOD_BLIVET:
            self._skip_to_spoke("BlivetGuiSpoke")
            return

        self._back_clicked = False
        return

    def _reset_to_snapshot(self):
        # Can we reset the storage configuration?
        reset = False

        # Changing disk selection is really, really complicated and has
        # always been causing numerous hard bugs. Let's not play the hero
        # game and just revert everything and start over again.
        disks = self._last_selected_disks
        current_disks = set(self._selected_disks)
        self._last_selected_disks = set(current_disks)

        if disks and disks != current_disks:
            log.info("Disk selection has changed.")
            reset = True

        method = self._last_partitioning_method
        current_method = self._get_selected_partitioning_method()
        self._last_partitioning_method = current_method

        # Same thing for switching between different storage configuration
        # methods (auto/custom/blivet-gui), at least for now.
        if method != current_method:
            log.info("Partitioning method has changed from %s to %s.", method,
                     current_method)
            reset = True

        # Reset the storage configuration if necessary.
        # FIXME: Reset only the partitioning that we will use.
        if reset:
            log.info("Rolling back planed storage configuration changes.")
            self._storage_module.ResetPartitioning()

    def _check_disk_selection(self):
        # If there are some disk selection errors we don't let user to leave
        # the spoke, so these errors don't have to go to self.errors.
        report = ValidationReport.from_structure(
            self._disk_select_module.ValidateSelectedDisks(
                self._selected_disks))

        if not report.is_valid():
            self._disks_errors = report.get_messages()
            self.set_error(
                _("There was a problem with your disk selection. "
                  "Click here for details."))
            return False

        self._disks_errors = []
        return True

    def _skip_to_spoke(self, name, apply_on_skip=True):
        """Skip to a spoke.

        The user has requested to skip to different spoke or to the
        summary hub.

        :param name: a name of the spoke or None to return to the hub
        :param apply_on_skip: should we call apply?
        """
        self.skipTo = name
        self.applyOnSkip = apply_on_skip
        NormalSpoke.on_back_clicked(self, None)

    def _skip_to_automatic_partitioning(self):
        """Skip to the automatic partitioning.

        The user has requested to create the partitioning automatically.
        Ask for missing information and set up the automatic partitioning,
        so it can be later applied in the execute method.
        """
        # Set up the encryption.
        self._partitioning_request.encrypted = self._encrypted_checkbox.get_active(
        )

        # Ask for a passphrase.
        if self._partitioning_request.encrypted:
            dialog = PassphraseDialog(self.data,
                                      self._partitioning_request.passphrase)

            rc = self.run_lightbox_dialog(dialog)
            if rc != 1:
                self._back_clicked = False
                return

            self._partitioning_request.passphrase = dialog.passphrase

        # Set up the disk selection and initialization.
        self.apply()

        # Use the automatic partitioning and reset it.
        self._partitioning = create_partitioning(PARTITIONING_METHOD_AUTOMATIC)

        self._partitioning.Request = \
            PartitioningRequest.to_structure(self._partitioning_request)

        # Reclaim space.
        disks = filter_disks_by_names(self._available_disks,
                                      self._selected_disks)
        rc = self._check_space_and_run_dialog(self._partitioning, disks)

        if rc == RESPONSE_RECLAIM:
            dialog = ResizeDialog(self.data, self.payload, self._partitioning,
                                  disks)
            dialog.refresh()
            rc = self.run_lightbox_dialog(dialog)

        # Plan the next action.
        if rc == RESPONSE_OK:
            # nothing special needed
            self._skip_to_spoke(None)
            return

        if rc == RESPONSE_CANCEL:
            # A cancel button was clicked on one of the dialogs.  Stay on this
            # spoke.  Generally, this is because the user wants to add more disks.
            self._back_clicked = False
            return

        if rc == RESPONSE_MODIFY_SW:
            # The "Fedora software selection" link was clicked on one of the
            # dialogs.  Send the user to the software spoke.
            self._skip_to_spoke("SoftwareSelectionSpoke")
            return

        if rc == RESPONSE_QUIT:
            # Not enough space, and the user can't do anything about it so
            # they chose to quit.
            raise SystemExit("user-selected exit")

        # I don't know how we'd get here, but might as well have a
        # catch-all.  Just stay on this spoke.
        self._back_clicked = False
        return

    def on_specialized_clicked(self, button):
        # Don't want to run apply or execute in this case, since we have to
        # collect some more disks first.  The user will be back to this spoke.
        self.applyOnSkip = False

        # However, we do want to apply current selections so the disk cart off
        # the filter spoke will display the correct information.
        apply_disk_selection(self._selected_disks)

        self.skipTo = "FilterSpoke"
        NormalSpoke.on_back_clicked(self, button)

    def on_info_bar_clicked(self, *args):
        if self._disks_errors:
            label = _(
                "The following errors were encountered when checking your disk "
                "selection. You can modify your selection or quit the "
                "installer.")

            dialog = DetailedErrorDialog(self.data,
                                         buttons=[
                                             C_("GUI|Storage|Error Dialog",
                                                "_Quit"),
                                             C_("GUI|Storage|Error Dialog",
                                                "_Modify Disk Selection")
                                         ],
                                         label=label)
            with self.main_window.enlightbox(dialog.window):
                errors = "\n".join(self._disks_errors)
                dialog.refresh(errors)
                rc = dialog.run()

            dialog.window.destroy()

            if rc == 0:
                # Quit.
                util.ipmi_abort(scripts=self.data.scripts)
                sys.exit(0)

        elif self.errors:
            label = _(
                "The following errors were encountered when checking your storage "
                "configuration.  You can modify your storage layout or quit the "
                "installer.")

            dialog = DetailedErrorDialog(self.data,
                                         buttons=[
                                             C_("GUI|Storage|Error Dialog",
                                                "_Quit"),
                                             C_("GUI|Storage|Error Dialog",
                                                "_Modify Storage Layout")
                                         ],
                                         label=label)
            with self.main_window.enlightbox(dialog.window):
                errors = "\n".join(self.errors)
                dialog.refresh(errors)
                rc = dialog.run()

            dialog.window.destroy()

            if rc == 0:
                # Quit.
                util.ipmi_abort(scripts=self.data.scripts)
                sys.exit(0)
        elif self.warnings:
            label = _(
                "The following warnings were encountered when checking your storage "
                "configuration.  These are not fatal, but you may wish to make "
                "changes to your storage layout.")

            dialog = DetailedErrorDialog(
                self.data,
                buttons=[C_("GUI|Storage|Warning Dialog", "_OK")],
                label=label)
            with self.main_window.enlightbox(dialog.window):
                warnings = "\n".join(self.warnings)
                dialog.refresh(warnings)
                rc = dialog.run()

            dialog.window.destroy()

    def on_disks_key_released(self, box, event):
        # we want to react only on Ctrl-A being pressed
        if not bool(event.state & Gdk.ModifierType.CONTROL_MASK) or \
                (event.keyval not in (Gdk.KEY_a, Gdk.KEY_A)):
            return

        # select disks in the right box
        if box is self._local_disks_box:
            overviews = self.local_overviews
        elif box is self._specialized_disks_box:
            overviews = self.advanced_overviews
        else:
            # no other box contains disk overviews
            return

        for overview in overviews:
            overview.set_chosen(True)

        self._update_disk_list()
        self._update_summary()

    # This callback is for the button that has anaconda go back and rescan the
    # disks to pick up whatever changes the user made outside our control.
    def on_refresh_clicked(self, *args):
        dialog = RefreshDialog(self.data)
        ignoreEscape(dialog.window)
        with self.main_window.enlightbox(dialog.window):
            rc = dialog.run()
            dialog.window.destroy()

        if rc == 1:
            # User hit OK on the dialog, indicating they stayed on the dialog
            # until rescanning completed.
            self.refresh()
            return
        elif rc != 2:
            # User either hit cancel on the dialog or closed it via escape, so
            # there was no rescanning done.
            # NOTE: rc == 2 means the user clicked on the link that takes them
            # back to the hub.
            return

        # Can't use this spoke's on_back_clicked method as that will try to
        # save the right hand side, which is no longer valid.  The user must
        # go back and select their disks all over again since whatever they
        # did on the shell could have changed what disks are available.
        NormalSpoke.on_back_clicked(self, None)
Пример #9
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)

        self._users_module = USERS.get_observer()
        self._users_module.connect()

        self._services_module = SERVICES.get_observer()
        self._services_module.connect()

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()
        # get object references from the builders
        self._password_entry = self.builder.get_object("password_entry")
        self._password_confirmation_entry = self.builder.get_object("password_confirmation_entry")
        self._password_bar = self.builder.get_object("password_bar")
        self._password_label = self.builder.get_object("password_label")

        # set state based on kickstart
        # NOTE: this will stop working once the module supports multiple kickstart commands
        self.password_kickstarted = self._users_module.proxy.IsRootpwKickstarted

        # Install the password checks:
        # - Has a password been specified?
        # - If a password has been specified and there is data in the confirm box, do they match?
        # - How strong is the password?
        # - Does the password contain non-ASCII characters?

        # Setup the password checker for password checking
        self._checker = input_checking.PasswordChecker(
                initial_password_content = self.password,
                initial_password_confirmation_content = self.password_confirmation,
                policy = input_checking.get_policy(self.data, "root")
        )
        # configure the checker for password checking
        self.checker.secret_type = constants.SecretType.PASSWORD
        # remove any placeholder texts if either password or confirmation field changes content from initial state
        self.checker.password.changed_from_initial_state.connect(self.remove_placeholder_texts)
        self.checker.password_confirmation.changed_from_initial_state.connect(self.remove_placeholder_texts)
        # connect UI updates to check results
        self.checker.checks_done.connect(self._checks_done)

        # check that the password is not empty
        self._empty_check = input_checking.PasswordEmptyCheck()
        # check that the content of the password field & the conformation field are the same
        self._confirm_check = input_checking.PasswordConfirmationCheck()
        # check password validity, quality and strength
        self._validity_check = input_checking.PasswordValidityCheck()
        # connect UI updates to validity check results
        self._validity_check.result.password_score_changed.connect(self.set_password_score)
        self._validity_check.result.status_text_changed.connect(self.set_password_status)
        # check if the password contains non-ascii characters
        self._ascii_check = input_checking.PasswordASCIICheck()

        # register the individual checks with the checker in proper order
        # 1) is the password non-empty ?
        # 2) are both entered passwords the same ?
        # 3) is the password valid according to the current password checking policy ?
        # 4) is the password free of non-ASCII characters ?
        self.checker.add_check(self._empty_check)
        self.checker.add_check(self._confirm_check)
        self.checker.add_check(self._validity_check)
        self.checker.add_check(self._ascii_check)

        # set placeholders if the password has been kickstarted as we likely don't know
        # nothing about it and can't really show it in the UI in any meaningful way
        password_set_message = _("The password was set by kickstart.")
        if self.password_kickstarted:
            self.password_entry.set_placeholder_text(password_set_message)
            self.password_confirmation_entry.set_placeholder_text(password_set_message)

        # Configure levels for the password bar
        self._password_bar.add_offset_value("low", 2)
        self._password_bar.add_offset_value("medium", 3)
        self._password_bar.add_offset_value("high", 4)

        # Send ready signal to main event loop
        hubQ.send_ready(self.__class__.__name__, False)

        # report that we are done
        self.initialize_done()

    def refresh(self):
        # focus on the password field if password was not kickstarted
        if not self.password_kickstarted:
            self.password_entry.grab_focus()

        # rerun checks so that we have a correct status message, if any
        self.checker.run_checks()

    @property
    def status(self):
        if self._users_module.proxy.IsRootAccountLocked:
            # check if we are running in Initial Setup reconfig mode
            reconfig_mode = self._services_module.proxy.SetupOnBoot == constants.SETUP_ON_BOOT_RECONFIG
            # reconfig mode currently allows re-enabling a locked root account if
            # user sets a new root password
            if reconfig_mode:
                return _("Disabled, set password to enable.")
            else:
                return _("Root account is disabled.")

        elif self._users_module.proxy.IsRootPasswordSet:
            return _("Root password is set")
        else:
            return _("Root password is not set")

    @property
    def mandatory(self):
        return not any(user for user in self.data.user.userList if "wheel" in user.groups)

    def apply(self):
        pw = self.password

        # value from the kickstart changed
        # NOTE: yet again, this stops to be valid once multiple
        #       commands are supported by a single DBUS module
        self._users_module.proxy.SetRootpwKickstarted(False)
        self.password_kickstarted = False

        self._users_module.proxy.SetRootAccountLocked(False)

        if not pw:
            self._users_module.proxy.ClearRootPassword()
            return

        # we have a password - set it to kickstart data

        self._users_module.proxy.SetCryptedRootPassword(cryptPassword(pw))

        # clear any placeholders
        self.remove_placeholder_texts()

        # Send ready signal to main event loop
        hubQ.send_ready(self.__class__.__name__, False)

    @property
    def completed(self):
        return bool(self._users_module.proxy.IsRootPasswordSet or self._users_module.proxy.IsRootAccountLocked)

    @property
    def sensitive(self):
        return not (self.completed and flags.automatedInstall
                    and self._users_module.proxy.IsRootpwKickstarted)

    def _checks_done(self, error_message):
        """Update the warning with the input validation error from the first
           error message or clear warnings if all the checks were successful.

           Also appends the "press twice" suffix if compatible with current
           password policy and handles the press-done-twice logic.
        """
        # check if an unwaivable check failed
        unwaivable_check_failed = not self._confirm_check.result.success

        # set appropriate status bar message
        if not error_message:
            # all is fine, just clear the message
            self.clear_info()
        elif not self.password and not self.password_confirmation:
            # Clear any info message if both the password and password
            # confirmation fields are empty.
            # This shortcut is done to make it possible for the user to leave the spoke
            # without inputting any root password. Separate logic makes sure an
            # empty string is not set as the root password.
            self.clear_info()
        else:
            if self.checker.policy.strict or unwaivable_check_failed:
                # just forward the error message
                self.show_warning_message(error_message)
            else:
                # add suffix for the click twice logic
                self.show_warning_message("{} {}".format(error_message,
                                                         _(constants.PASSWORD_DONE_TWICE)))

        # check if the spoke can be exited after the latest round of checks
        self._check_spoke_exit_conditions(unwaivable_check_failed)

    def _check_spoke_exit_conditions(self, unwaivable_check_failed):
        # Check if the user can escape from the root spoke or stay forever !

        # reset any waiving in progress
        self.waive_clicks = 0

        # Depending on the policy we allow users to waive the password strength
        # and non-ASCII checks. If the policy is set to strict, the password
        # needs to be strong, but can still contain non-ASCII characters.
        self.can_go_back = False
        self.needs_waiver = True

        # This shortcut is done to make it possible for the user to leave the spoke
        # without inputting any root password. Separate logic makes sure an
        # empty string is not set as the root password.
        if not self.password and not self.password_confirmation:
            self.can_go_back = True
            self.needs_waiver = False
        elif self.checker.success:
            # if all checks were successful we can always go back to the hub
            self.can_go_back = True
            self.needs_waiver = False
        elif unwaivable_check_failed:
            self.can_go_back = False
        else:
            if self.checker.policy.strict:
                if not self._validity_check.result.success:
                    # failing validity check in strict
                    # mode prevents us from going back
                    self.can_go_back = False
                elif not self._ascii_check.result.success:
                    # but the ASCII check can still be waived
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False
            else:
                if not self._validity_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                elif not self._ascii_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False

    def on_password_changed(self, editable, data=None):
        """Tell checker that the content of the password field changed."""
        self.checker.password.content = self.password

    def on_password_confirmation_changed(self, editable, data=None):
        """Tell checker that the content of the password confirmation field changed."""
        self.checker.password_confirmation.content = self.password_confirmation

    def on_password_icon_clicked(self, entry, icon_pos, event):
        """Called by Gtk callback when the icon of a password entry is clicked."""
        set_password_visibility(entry, not entry.get_visibility())

    def on_back_clicked(self, button):
        # the GUI spoke input check handler handles the spoke exit logic for us
        if self.try_to_go_back():
            NormalSpoke.on_back_clicked(self, button)
        else:
            log.info("Return to hub prevented by password checking rules.")
Пример #10
0
def get_container_type(device_type):
    return CONTAINER_TYPES.get(device_type, ContainerType(N_("container"), CN_(
        "GUI|Custom Partitioning|Configure|Devices", "container")))
Пример #11
0
NOTEBOOK_LUKS_PAGE = 2
NOTEBOOK_UNEDITABLE_PAGE = 3
NOTEBOOK_INCOMPLETE_PAGE = 4

NEW_CONTAINER_TEXT = N_("Create a new %(container_type)s ...")
CONTAINER_TOOLTIP = N_("Create or select %(container_type)s")
CONTAINER_DIALOG_TITLE = N_("CONFIGURE %(container_type)s")
CONTAINER_DIALOG_TEXT = N_("Please create a name for this %(container_type)s "
                           "and select at least one disk below.")

ContainerType = namedtuple("ContainerType", ["name", "label"])

CONTAINER_TYPES = {
    DEVICE_TYPE_LVM: ContainerType(
        N_("Volume Group"),
        CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume Group:")),
    DEVICE_TYPE_LVM_THINP: ContainerType(
        N_("Volume Group"),
        CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume Group:")),
    DEVICE_TYPE_BTRFS: ContainerType(
        N_("Volume"),
        CN_("GUI|Custom Partitioning|Configure|Devices", "_Volume:"))
}


def get_size_from_entry(entry, lower_bound=None, units=None):
    """ Get a Size object from an entry field.

        :param lower_bound: lower bound for size returned,
        :type lower_bound: :class:`blivet.size.Size` or NoneType
        :param units: units to use if none obtained from entry
Пример #12
0
class FilterSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: FilterSpoke
          :parts: 3
    """
    builderObjects = ["diskStore", "filterWindow",
                      "searchModel", "multipathModel", "otherModel", "zModel", "nvdimmModel"]
    mainWidgetName = "filterWindow"
    uiFile = "spokes/advanced_storage.glade"
    helpFile = "FilterSpoke.xml"

    category = SystemCategory

    title = CN_("GUI|Spoke", "_Installation Destination")

    @staticmethod
    def get_screen_id():
        """Return a unique id of this UI screen."""
        return "storage-advanced-configuration"

    def __init__(self, *args):
        super().__init__(*args)
        self.applyOnSkip = True

        self._pages = {}
        self._ancestors = []
        self._disks = []
        self._selected_disks = []
        self._protected_disks = []

        self._storage_module = STORAGE.get_proxy()
        self._device_tree = STORAGE.get_proxy(DEVICE_TREE)
        self._disk_selection = STORAGE.get_proxy(DISK_SELECTION)

        self._notebook = self.builder.get_object("advancedNotebook")
        self._store = self.builder.get_object("diskStore")
        self._reconfigure_nvdimm_button = self.builder.get_object("reconfigureNVDIMMButton")

    @property
    def indirect(self):
        return True

    # This spoke has no status since it's not in a hub
    @property
    def status(self):
        return None

    def apply(self):
        apply_disk_selection(self._selected_disks)

    def initialize(self):
        super().initialize()
        self.initialize_start()

        self._pages = {
            PAGE_SEARCH: SearchPage(self.builder),
            PAGE_MULTIPATH: MultipathPage(self.builder),
            PAGE_OTHER: OtherPage(self.builder),
            PAGE_NVDIMM: NvdimmPage(self.builder),
            PAGE_Z: ZPage(self.builder),
        }

        if not STORAGE.get_proxy(DASD).IsSupported():
            self._notebook.remove_page(PAGE_Z)
            self._pages.pop(PAGE_Z)

            self.builder.get_object("addZFCPButton").destroy()
            self.builder.get_object("addDASDButton").destroy()

        if not STORAGE.get_proxy(FCOE).IsSupported():
            self.builder.get_object("addFCOEButton").destroy()

        if not STORAGE.get_proxy(ISCSI).IsSupported():
            self.builder.get_object("addISCSIButton").destroy()

        # The button is sensitive only on NVDIMM page
        self._reconfigure_nvdimm_button.set_sensitive(False)

        # report that we are done
        self.initialize_done()

    def refresh(self):
        super().refresh()

        # Reset the scheduled partitioning if any to make sure that we
        # are working with the current system’s storage configuration.
        # FIXME: Change modules and UI to work with the right device tree.
        self._storage_module.ResetPartitioning()

        self._disks = self._disk_selection.GetUsableDisks()
        self._selected_disks = self._disk_selection.SelectedDisks
        self._protected_disks = self._disk_selection.ProtectedDevices
        self._ancestors = self._device_tree.GetAncestors(self._disks)

        # Now all all the non-local disks to the store.  Everything has been set up
        # ahead of time, so there's no need to configure anything.  We first make
        # these lists of disks, then call setup on each individual page.  This is
        # because there could be page-specific setup to do that requires a complete
        # view of all the disks on that page.
        self._store.clear()

        disks_data = DeviceData.from_structure_list([
            self._device_tree.GetDeviceData(device_name)
            for device_name in self._disks
        ])

        for page in self._pages.values():
            disks = [
                d for d in disks_data
                if page.is_member(d.type)
            ]

            page.setup(
                self._store,
                disks,
                self._selected_disks,
                self._protected_disks
            )

        self._update_summary()

    def _update_summary(self):
        summary_button = self.builder.get_object("summary_button")
        label = self.builder.get_object("summary_button_label")

        # We need to remove ancestor devices from the count.  Otherwise, we'll
        # end up in a situation where selecting one multipath device could
        # potentially show three devices selected (mpatha, sda, sdb for instance).
        count = len([
            disk for disk in self._selected_disks
            if disk not in self._ancestors
        ])

        summary = CP_(
            "GUI|Installation Destination|Filter",
            "{} _storage device selected",
            "{} _storage devices selected",
            count
        ).format(count)

        if count > 0:
            really_show(summary_button)
            label.set_text(summary)
            label.set_use_underline(True)
        else:
            really_hide(summary_button)

    def on_back_clicked(self, button):
        self.skipTo = "StorageSpoke"
        super().on_back_clicked(button)

    def on_summary_clicked(self, button):
        disks = filter_disks_by_names(
            self._disks, self._selected_disks
        )
        dialog = SelectedDisksDialog(
            self.data, disks, show_remove=False, set_boot=False
        )

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

    def on_clear_icon_clicked(self, entry, icon_pos, event):
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
            entry.set_text("")

    def on_page_switched(self, notebook, new_page, new_page_num, *args):
        # Disable all filters.
        for page in self._pages.values():
            page.is_active = False

        # Set up the new page.
        page = self._pages[new_page_num]
        page.is_active = True
        page.model.refilter()

        log.debug("Show the page %s.", str(page))

        # Set up the UI.
        notebook.get_nth_page(new_page_num).show_all()
        self._reconfigure_nvdimm_button.set_sensitive(new_page_num == 3)

    def on_row_toggled(self, button, path):
        if not path:
            return

        page_index = self._notebook.get_current_page()
        filter_model = self._pages[page_index].model
        model_itr = filter_model.get_iter(path)
        itr = filter_model.convert_iter_to_child_iter(model_itr)
        self._store[itr][1] = not self._store[itr][1]

        if self._store[itr][1] and self._store[itr][3] not in self._selected_disks:
            self._selected_disks.append(self._store[itr][3])
        elif not self._store[itr][1] and self._store[itr][3] in self._selected_disks:
            self._selected_disks.remove(self._store[itr][3])

        self._update_summary()

    @timed_action(delay=50, threshold=100)
    def on_refresh_clicked(self, widget, *args):
        log.debug("Refreshing...")
        try_populate_devicetree()
        self.refresh()

    def on_add_iscsi_clicked(self, widget, *args):
        log.debug("Add a new iSCSI device.")
        dialog = ISCSIDialog(self.data)
        self._run_dialog_and_refresh(dialog)

    def on_add_fcoe_clicked(self, widget, *args):
        log.debug("Add a new FCoE device.")
        dialog = FCoEDialog(self.data)
        self._run_dialog_and_refresh(dialog)

    def on_add_zfcp_clicked(self, widget, *args):
        log.debug("Add a new zFCP device.")
        dialog = ZFCPDialog(self.data)
        self._run_dialog_and_refresh(dialog)

    def on_add_dasd_clicked(self, widget, *args):
        log.debug("Add a new DASD device.")
        dialog = DASDDialog(self.data)
        self._run_dialog_and_refresh(dialog)

    def on_reconfigure_nvdimm_clicked(self, widget, *args):
        log.debug("Reconfigure a NVDIMM device.")
        namespaces = self._pages[PAGE_NVDIMM].get_selected_namespaces()
        dialog = NVDIMMDialog(self.data, namespaces)
        self._run_dialog_and_refresh(dialog)

    def _run_dialog_and_refresh(self, dialog):
        # Run the dialog.
        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    @timed_action(delay=1200, busy_cursor=False)
    def on_filter_changed(self, *args):
        self._refilter_current_page()

    def on_search_type_changed(self, combo):
        self._set_notebook_page("searchTypeNotebook", combo.get_active())
        self._refilter_current_page()

    def on_multipath_type_changed(self, combo):
        self._set_notebook_page("multipathTypeNotebook", combo.get_active())
        self._refilter_current_page()

    def on_other_type_combo_changed(self, combo):
        self._set_notebook_page("otherTypeNotebook", combo.get_active())
        self._refilter_current_page()

    def on_nvdimm_type_combo_changed(self, combo):
        self._set_notebook_page("nvdimmTypeNotebook", combo.get_active())
        self._refilter_current_page()

    def on_z_type_combo_changed(self, combo):
        self._set_notebook_page("zTypeNotebook", combo.get_active())
        self._refilter_current_page()

    def _set_notebook_page(self, notebook_name, page_index):
        notebook = self.builder.get_object(notebook_name)
        notebook.set_current_page(page_index)
        self._refilter_current_page()

    def _refilter_current_page(self):
        index = self._notebook.get_current_page()
        page = self._pages[index]
        page.model.refilter()
Пример #13
0
class FilterSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: FilterSpoke
          :parts: 3
    """
    builderObjects = [
        "diskStore", "filterWindow", "searchModel", "multipathModel",
        "otherModel", "zModel"
    ]
    mainWidgetName = "filterWindow"
    uiFile = "spokes/advanced_storage.glade"
    helpFile = "FilterSpoke.xml"

    category = SystemCategory

    title = CN_("GUI|Spoke", "_INSTALLATION DESTINATION")

    def __init__(self, *args):
        NormalSpoke.__init__(self, *args)
        self.applyOnSkip = True

        self.ancestors = []
        self.disks = []
        self.selected_disks = []

    @property
    def indirect(self):
        return True

    # This spoke has no status since it's not in a hub
    @property
    def status(self):
        return None

    def apply(self):
        applyDiskSelection(self.storage, self.data, self.selected_disks)

        # some disks may have been added in this spoke, we need to recreate the
        # snapshot of on-disk storage
        if on_disk_storage.created:
            on_disk_storage.dispose_snapshot()
        on_disk_storage.create_snapshot(self.storage)

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()

        self.pages = [
            SearchPage(self.storage, self.builder),
            MultipathPage(self.storage, self.builder),
            OtherPage(self.storage, self.builder),
            ZPage(self.storage, self.builder)
        ]

        self._notebook = self.builder.get_object("advancedNotebook")

        if not arch.is_s390():
            self._notebook.remove_page(-1)
            self.builder.get_object("addZFCPButton").destroy()
            self.builder.get_object("addDASDButton").destroy()

        if not has_fcoe():
            self.builder.get_object("addFCOEButton").destroy()

        if not iscsi.available:
            self.builder.get_object("addISCSIButton").destroy()

        self._store = self.builder.get_object("diskStore")
        self._addDisksButton = self.builder.get_object("addDisksButton")

        # report that we are done
        self.initialize_done()

    def _real_ancestors(self, disk):
        # Return a list of all the ancestors of a disk, but remove the disk
        # itself from this list.
        return [d for d in disk.ancestors if d.name != disk.name]

    def refresh(self):
        NormalSpoke.refresh(self)

        self.disks = getDisks(self.storage.devicetree)
        self.selected_disks = self.data.ignoredisk.onlyuse[:]

        self.ancestors = [
            d.name for disk in self.disks for d in self._real_ancestors(disk)
        ]

        self._store.clear()

        allDisks = []
        multipathDisks = []
        otherDisks = []
        zDisks = []

        # Now all all the non-local disks to the store.  Everything has been set up
        # ahead of time, so there's no need to configure anything.  We first make
        # these lists of disks, then call setup on each individual page.  This is
        # because there could be page-specific setup to do that requires a complete
        # view of all the disks on that page.
        for disk in self.disks:
            if self.pages[1].ismember(disk):
                multipathDisks.append(disk)
            elif self.pages[2].ismember(disk):
                otherDisks.append(disk)
            elif self.pages[3].ismember(disk):
                zDisks.append(disk)

            allDisks.append(disk)

        self.pages[0].setup(self._store, self.selected_disks, allDisks)
        self.pages[1].setup(self._store, self.selected_disks, multipathDisks)
        self.pages[2].setup(self._store, self.selected_disks, otherDisks)
        self.pages[3].setup(self._store, self.selected_disks, zDisks)

        self._update_summary()

    def _update_summary(self):
        summaryButton = self.builder.get_object("summary_button")
        label = self.builder.get_object("summary_button_label")

        # We need to remove ancestor devices from the count.  Otherwise, we'll
        # end up in a situation where selecting one multipath device could
        # potentially show three devices selected (mpatha, sda, sdb for instance).
        count = len([
            disk for disk in self.selected_disks if disk not in self.ancestors
        ])

        summary = CP_("GUI|Installation Destination|Filter",
                      "%d _storage device selected",
                      "%d _storage devices selected", count) % count

        label.set_text(summary)
        label.set_use_underline(True)

        summaryButton.set_visible(count > 0)
        label.set_sensitive(count > 0)

    def on_back_clicked(self, button):
        self.skipTo = "StorageSpoke"
        NormalSpoke.on_back_clicked(self, button)

    def on_summary_clicked(self, button):
        dialog = SelectedDisksDialog(self.data)

        # Include any disks selected in the initial storage spoke, plus any
        # selected in this filter UI.
        disks = [
            disk for disk in self.disks if disk.name in self.selected_disks
        ]
        free_space = self.storage.get_free_space(disks=disks)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh(disks, free_space, showRemove=False, setBoot=False)
            dialog.run()

    @timed_action(delay=1200, busy_cursor=False)
    def on_filter_changed(self, *args):
        n = self._notebook.get_current_page()
        self.pages[n].filterActive = True
        self.pages[n].model.refilter()

    def on_clear_icon_clicked(self, entry, icon_pos, event):
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
            entry.set_text("")

    def on_page_switched(self, notebook, newPage, newPageNum, *args):
        self.pages[newPageNum].model.refilter()
        notebook.get_nth_page(newPageNum).show_all()

    def on_row_toggled(self, button, path):
        if not path:
            return

        page_index = self._notebook.get_current_page()
        filter_model = self.pages[page_index].model
        model_itr = filter_model.get_iter(path)
        itr = filter_model.convert_iter_to_child_iter(model_itr)
        self._store[itr][1] = not self._store[itr][1]

        if self._store[itr][1] and self._store[itr][
                3] not in self.selected_disks:
            self.selected_disks.append(self._store[itr][3])
        elif not self._store[itr][1] and self._store[itr][
                3] in self.selected_disks:
            self.selected_disks.remove(self._store[itr][3])

        self._update_summary()

    @timed_action(delay=50, threshold=100)
    def on_refresh_clicked(self, widget, *args):
        try_populate_devicetree(self.storage.devicetree)
        self.refresh()

    def on_add_iscsi_clicked(self, widget, *args):
        dialog = ISCSIDialog(self.data, self.storage)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    def on_add_fcoe_clicked(self, widget, *args):
        dialog = FCoEDialog(self.data, self.storage)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    def on_add_zfcp_clicked(self, widget, *args):
        dialog = ZFCPDialog(self.data, self.storage)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    def on_add_dasd_clicked(self, widget, *args):
        dialog = DASDDialog(self.data, self.storage)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    ##
    ## SEARCH TAB SIGNAL HANDLERS
    ##
    def on_search_type_changed(self, combo):
        ndx = combo.get_active()

        notebook = self.builder.get_object("searchTypeNotebook")

        notebook.set_current_page(ndx)
        self.on_filter_changed()

    ##
    ## MULTIPATH TAB SIGNAL HANDLERS
    ##
    def on_multipath_type_changed(self, combo):
        ndx = combo.get_active()

        notebook = self.builder.get_object("multipathTypeNotebook")

        notebook.set_current_page(ndx)
        self.on_filter_changed()

    ##
    ## OTHER TAB SIGNAL HANDLERS
    ##
    def on_other_type_combo_changed(self, combo):
        ndx = combo.get_active()

        notebook = self.builder.get_object("otherTypeNotebook")

        notebook.set_current_page(ndx)
        self.on_filter_changed()

    ##
    ## Z TAB SIGNAL HANDLERS
    ##
    def on_z_type_combo_changed(self, combo):
        ndx = combo.get_active()

        notebook = self.builder.get_object("zTypeNotebook")

        notebook.set_current_page(ndx)
        self.on_filter_changed()
Пример #14
0
class FilterSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: FilterSpoke
          :parts: 3
    """
    builderObjects = [
        "diskStore", "filterWindow", "searchModel", "multipathModel",
        "otherModel", "zModel", "nvdimmModel"
    ]
    mainWidgetName = "filterWindow"
    uiFile = "spokes/advanced_storage.glade"
    helpFile = "FilterSpoke.xml"

    category = SystemCategory

    title = CN_("GUI|Spoke", "_Installation Destination")

    def __init__(self, *args):
        super().__init__(*args)
        self.applyOnSkip = True

        self._pages = {}
        self._ancestors = []
        self._disks = []
        self._selected_disks = []

        self._disk_selection = STORAGE.get_proxy(DISK_SELECTION)

        self._notebook = self.builder.get_object("advancedNotebook")
        self._store = self.builder.get_object("diskStore")
        self._reconfigure_nvdimm_button = self.builder.get_object(
            "reconfigureNVDIMMButton")

    @property
    def indirect(self):
        return True

    # This spoke has no status since it's not in a hub
    @property
    def status(self):
        return None

    def apply(self):
        apply_disk_selection(self.storage, self._selected_disks)

        # some disks may have been added in this spoke, we need to recreate the
        # snapshot of on-disk storage
        if on_disk_storage.created:
            on_disk_storage.dispose_snapshot()
        on_disk_storage.create_snapshot(self.storage)

    def initialize(self):
        super().initialize()
        self.initialize_start()

        self._pages = {
            PAGE_SEARCH: SearchPage(self.storage, self.builder),
            PAGE_MULTIPATH: MultipathPage(self.storage, self.builder),
            PAGE_OTHER: OtherPage(self.storage, self.builder),
            PAGE_NVDIMM: NvdimmPage(self.storage, self.builder),
            PAGE_Z: ZPage(self.storage, self.builder),
        }

        if not STORAGE.get_proxy(DASD).IsSupported():
            self._notebook.remove_page(PAGE_Z)
            self._pages.pop(PAGE_Z)
            self.builder.get_object("addZFCPButton").destroy()
            self.builder.get_object("addDASDButton").destroy()

        if not STORAGE.get_proxy(FCOE).IsSupported():
            self.builder.get_object("addFCOEButton").destroy()

        if not STORAGE.get_proxy(ISCSI).IsSupported():
            self.builder.get_object("addISCSIButton").destroy()

        # The button is sensitive only on NVDIMM page
        self._reconfigure_nvdimm_button.set_sensitive(False)

        # report that we are done
        self.initialize_done()

    def _real_ancestors(self, disk):
        # Return a list of all the ancestors of a disk, but remove the disk
        # itself from this list.
        return [d for d in disk.ancestors if d.name != disk.name]

    def refresh(self):
        super().refresh()
        self._disks = self.storage.usable_disks
        self._selected_disks = self._disk_selection.SelectedDisks

        self._ancestors = [
            d.name for disk in self._disks for d in self._real_ancestors(disk)
        ]

        # Now all all the non-local disks to the store.  Everything has been set up
        # ahead of time, so there's no need to configure anything.  We first make
        # these lists of disks, then call setup on each individual page.  This is
        # because there could be page-specific setup to do that requires a complete
        # view of all the disks on that page.
        self._store.clear()

        for page in self._pages.values():
            page.setup(
                self._store,
                self._selected_disks,
                list(filter(page.is_member, self._disks)),
            )

        self._update_summary()

    def _update_summary(self):
        summary_button = self.builder.get_object("summary_button")
        label = self.builder.get_object("summary_button_label")

        # We need to remove ancestor devices from the count.  Otherwise, we'll
        # end up in a situation where selecting one multipath device could
        # potentially show three devices selected (mpatha, sda, sdb for instance).
        count = len([
            disk for disk in self._selected_disks
            if disk not in self._ancestors
        ])

        summary = CP_("GUI|Installation Destination|Filter",
                      "{} _storage device selected",
                      "{} _storage devices selected", count).format(count)

        label.set_text(summary)
        label.set_use_underline(True)

        summary_button.set_visible(count > 0)
        label.set_sensitive(count > 0)

    def on_back_clicked(self, button):
        self.skipTo = "StorageSpoke"
        super().on_back_clicked(button)

    def on_summary_clicked(self, button):
        disks = filter_disks_by_names(self._disks, self._selected_disks)
        dialog = SelectedDisksDialog(self.data,
                                     self.storage,
                                     disks,
                                     show_remove=False,
                                     set_boot=False)

        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

    @timed_action(delay=1200, busy_cursor=False)
    def on_filter_changed(self, *args):
        n = self._notebook.get_current_page()
        self._pages[n].is_active = True
        self._pages[n].model.refilter()

    def on_clear_icon_clicked(self, entry, icon_pos, event):
        if icon_pos == Gtk.EntryIconPosition.SECONDARY:
            entry.set_text("")

    def on_page_switched(self, notebook, new_page, new_page_num, *args):
        self._pages[new_page_num].model.refilter()
        notebook.get_nth_page(new_page_num).show_all()
        self._reconfigure_nvdimm_button.set_sensitive(new_page_num == 3)

    def on_row_toggled(self, button, path):
        if not path:
            return

        page_index = self._notebook.get_current_page()
        filter_model = self._pages[page_index].model
        model_itr = filter_model.get_iter(path)
        itr = filter_model.convert_iter_to_child_iter(model_itr)
        self._store[itr][1] = not self._store[itr][1]

        if self._store[itr][1] and self._store[itr][
                3] not in self._selected_disks:
            self._selected_disks.append(self._store[itr][3])
        elif not self._store[itr][1] and self._store[itr][
                3] in self._selected_disks:
            self._selected_disks.remove(self._store[itr][3])

        self._update_summary()

    @timed_action(delay=50, threshold=100)
    def on_refresh_clicked(self, widget, *args):
        try_populate_devicetree(self.storage.devicetree)
        self.refresh()

    def on_add_iscsi_clicked(self, widget, *args):
        dialog = ISCSIDialog(self.data, self.storage)
        self._run_dialog_and_refresh(dialog)

    def on_add_fcoe_clicked(self, widget, *args):
        dialog = FCoEDialog(self.data, self.storage)
        self._run_dialog_and_refresh(dialog)

    def on_add_zfcp_clicked(self, widget, *args):
        dialog = ZFCPDialog(self.data, self.storage)
        self._run_dialog_and_refresh(dialog)

    def on_add_dasd_clicked(self, widget, *args):
        dialog = DASDDialog(self.data, self.storage)
        self._run_dialog_and_refresh(dialog)

    def on_reconfigure_nvdimm_clicked(self, widget, *args):
        namespaces = self._pages[PAGE_NVDIMM].get_selected_namespaces()
        dialog = NVDIMMDialog(self.data, self.storage, namespaces)
        self._run_dialog_and_refresh(dialog)

    def _run_dialog_and_refresh(self, dialog):
        # Run the dialog.
        with self.main_window.enlightbox(dialog.window):
            dialog.refresh()
            dialog.run()

        # We now need to refresh so any new disks picked up by adding advanced
        # storage are displayed in the UI.
        self.refresh()

    def on_search_type_changed(self, combo):
        self._set_notebook_page("searchTypeNotebook", combo.get_active())

    def on_multipath_type_changed(self, combo):
        self._set_notebook_page("multipathTypeNotebook", combo.get_active())

    def on_other_type_combo_changed(self, combo):
        self._set_notebook_page("otherTypeNotebook", combo.get_active())

    def on_nvdimm_type_combo_changed(self, combo):
        self._set_notebook_page("nvdimmTypeNotebook", combo.get_active())

    def on_z_type_combo_changed(self, combo):
        self._set_notebook_page("zTypeNotebook", combo.get_active())

    def _set_notebook_page(self, notebook_name, page_index):
        notebook = self.builder.get_object(notebook_name)
        notebook.set_current_page(page_index)
        self.on_filter_changed()
Пример #15
0
class SoftwareSelectionSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: SoftwareSelectionSpoke
          :parts: 3
    """
    builderObjects = ["addonStore", "environmentStore", "softwareWindow"]
    mainWidgetName = "softwareWindow"
    uiFile = "spokes/software_selection.glade"
    helpFile = "SoftwareSpoke.xml"

    category = SoftwareCategory

    icon = "package-x-generic-symbolic"
    title = CN_("GUI|Spoke", "_Software Selection")

    # Add-on selection states
    # no user interaction with this add-on
    _ADDON_DEFAULT = 0
    # user selected
    _ADDON_SELECTED = 1
    # user de-selected
    _ADDON_DESELECTED = 2

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._errorMsgs = None
        self._tx_id = None
        self._selectFlag = False

        self._environmentListBox = self.builder.get_object(
            "environmentListBox")
        self._addonListBox = self.builder.get_object("addonListBox")

        # Connect viewport scrolling with listbox focus events
        environmentViewport = self.builder.get_object("environmentViewport")
        addonViewport = self.builder.get_object("addonViewport")
        self._environmentListBox.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(environmentViewport))
        self._addonListBox.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(addonViewport))

        # Used to store how the user has interacted with add-ons for the default add-on
        # selection logic. The dictionary keys are group IDs, and the values are selection
        # state constants. See refreshAddons for how the values are used.
        self._addonStates = {}

        # Create a RadioButton that will never be displayed to use as the group for the
        # environment radio buttons. This way the environment radio buttons can all appear
        # unselected in the case of modifying data from kickstart.
        self._firstRadio = Gtk.RadioButton(group=None)

        # Used for detecting whether anything's changed in the spoke.
        self._origAddons = []
        self._origEnvironment = None

        # Whether we are using package selections from a kickstart
        self._kickstarted = flags.automatedInstall and self.data.packages.seen

        # Whether the payload is in an error state
        self._error = False

        # Register event listeners to update our status on payload events
        payloadMgr.addListener(payloadMgr.STATE_PACKAGE_MD,
                               self._downloading_package_md)
        payloadMgr.addListener(payloadMgr.STATE_GROUP_MD,
                               self._downloading_group_md)
        payloadMgr.addListener(payloadMgr.STATE_FINISHED,
                               self._payload_finished)
        payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error)

        # Add an invisible radio button so that we can show the environment
        # list with no radio buttons ticked
        self._fakeRadio = Gtk.RadioButton(group=None)
        self._fakeRadio.set_active(True)

    # Payload event handlers
    def _downloading_package_md(self):
        # Reset the error state from previous payloads
        self._error = False

        hubQ.send_message(self.__class__.__name__,
                          _(constants.PAYLOAD_STATUS_PACKAGE_MD))

    def _downloading_group_md(self):
        hubQ.send_message(self.__class__.__name__,
                          _(constants.PAYLOAD_STATUS_GROUP_MD))

    @property
    def environment(self):
        """A wrapper for the environment specification in kickstart"""
        return self.data.packages.environment

    @environment.setter
    def environment(self, value):
        self.data.packages.environment = value

    @property
    def environmentid(self):
        """Return the "machine readable" environment id

        Alternatively we could have just "canonicalized" the
        environment description to the "machine readable" format
        when reading it from kickstart for the first time.
        But this could result in input and output kickstart,
        which would be rather confusing for the user.
        So we don't touch the specification from kickstart
        if it is valid and use this property when we need
        the "machine readable" form.
        """
        if self.environment is None:
            # None means environment is not set, no need to try translate that to an id
            return None
        elif self.environment is False:
            # False means environment is not valid and must be set manually
            return False
        try:
            return self.payload.environmentId(self.environment)
        except NoSuchGroup:
            return None

    @property
    def environment_valid(self):
        """Return if the currently set environment is valid
        (represents an environment known by the payload)
        """
        # None means the environment has not been set by the user,
        # which means:
        # * set the default environment during interactive installation
        # * ask user to specify an environment during kickstart installation
        if self.environment is None:
            return True
        else:
            return self.environmentid in self.payload.environments

    def _payload_finished(self):
        if self.environment_valid:
            log.info("using environment from kickstart: %s", self.environment)
        else:
            log.error(
                "unknown environment has been specified in kickstart and will be ignored: %s",
                self.data.packages.environment)
            # False means that the environment has been set to an invalid value and needs to
            # be manually set to a valid one.
            self.environment = False

    def _payload_error(self):
        self._error = True
        hubQ.send_message(self.__class__.__name__, payloadMgr.error)

    def _apply(self):
        # Environment needs to be set during a GUI installation, but is not required
        # for a kickstart install (even partial)
        if not self.environment:
            log.debug("Environment is not set, skip user packages settings")
            return

        # NOTE: This block is skipped for kickstart where addons and _origAddons will
        # both be [], preventing it from wiping out the kickstart's package selection
        addons = self._get_selected_addons()
        if not self._kickstarted and set(addons) != set(self._origAddons):
            self._selectFlag = False
            self.payload.data.packages.packageList = []
            self.payload.data.packages.groupList = []
            self.payload.selectEnvironment(self.environment)
            log.debug("Environment selected for installation: %s",
                      self.environment)
            log.debug("Groups selected for installation: %s", addons)
            for group in addons:
                self.payload.selectGroup(group)

            # And then save these values so we can check next time.
            self._origAddons = addons
            self._origEnvironment = self.environment

        hubQ.send_not_ready(self.__class__.__name__)
        hubQ.send_not_ready("SourceSpoke")
        threadMgr.add(
            AnacondaThread(name=constants.THREAD_CHECK_SOFTWARE,
                           target=self.checkSoftwareSelection))

    def apply(self):
        self._apply()

    def checkSoftwareSelection(self):
        from pyanaconda.payload import DependencyError
        hubQ.send_message(self.__class__.__name__,
                          _("Checking software dependencies..."))
        try:
            self.payload.checkSoftwareSelection()
        except DependencyError as e:
            self._errorMsgs = str(e)
            hubQ.send_message(self.__class__.__name__,
                              _("Error checking software dependencies"))
            self._tx_id = None
        else:
            self._errorMsgs = None
            self._tx_id = self.payload.txID
        finally:
            hubQ.send_ready(self.__class__.__name__, False)
            hubQ.send_ready("SourceSpoke", False)

    @property
    def completed(self):
        processingDone = bool(
            not threadMgr.get(constants.THREAD_CHECK_SOFTWARE)
            and not threadMgr.get(constants.THREAD_PAYLOAD)
            and not self._errorMsgs and self.txid_valid)

        # * we should always check processingDone before checking the other variables,
        #   as they might be inconsistent until processing is finished
        # * we can't let the installation proceed until a valid environment has been set
        if processingDone:
            if self.environment is not None:
                # if we have environment it needs to be valid
                return self.environment_valid
            # if we don't have environment we need to at least have the %packages
            # section in kickstart
            elif self._kickstarted:
                return True
            # no environment and no %packages section -> manual intervention is needed
            else:
                return False
        else:
            return False

    @property
    def changed(self):
        if not self.environment:
            return True

        addons = self._get_selected_addons()

        # Don't redo dep solving if nothing's changed.
        if self.environment == self._origEnvironment and set(addons) == set(self._origAddons) and \
           self.txid_valid:
            return False

        return True

    @property
    def mandatory(self):
        return True

    @property
    def ready(self):
        # By default, the software selection spoke is not ready.  We have to
        # wait until the installation source spoke is completed.  This could be
        # because the user filled something out, or because we're done fetching
        # repo metadata from the mirror list, or we detected a DVD/CD.

        return bool(not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER)
                    and not threadMgr.get(constants.THREAD_PAYLOAD)
                    and not threadMgr.get(constants.THREAD_CHECK_SOFTWARE)
                    and self.payload.baseRepo is not None)

    @property
    def showable(self):
        return isinstance(self.payload, PackagePayload)

    @property
    def status(self):
        if self._errorMsgs:
            return _("Error checking software selection")

        if not self.ready:
            return _("Installation source not set up")

        if not self.txid_valid:
            return _("Source changed - please verify")

        # kickstart installation
        if flags.automatedInstall:
            if self._kickstarted:
                # %packages section is present in kickstart but environment is not set
                if self.environment is None:
                    return _("Custom software selected")
                # environment is set to an invalid value
                elif not self.environment_valid:
                    return _("Invalid environment specified in kickstart")
            # we have no packages section in the kickstart and no environment has been set
            elif not self.environment:
                return _("Nothing selected")

        if not flags.automatedInstall:
            if not self.environment:
                # No environment yet set
                return _("Nothing selected")
            elif not self.environment_valid:
                # selected environment is not valid, this can happen when a valid environment
                # is selected (by default, manually or from kickstart) and then the installation
                # source is switched to one where the selected environment is no longer valid
                return _("Selected environment is not valid")

        return self.payload.environmentDescription(self.environment)[0]

    def initialize(self):
        super().initialize()
        self.initialize_start()
        threadMgr.add(
            AnacondaThread(name=constants.THREAD_SOFTWARE_WATCHER,
                           target=self._initialize))

    def _initialize(self):
        threadMgr.wait(constants.THREAD_PAYLOAD)
        # Select groups which should be selected by kickstart
        try:
            for group in self.payload.selectedGroupsIDs():
                if self.environment and self.payload.environmentOptionIsDefault(
                        self.environment, group):
                    self._addonStates[group] = self._ADDON_DEFAULT
                else:
                    self._addonStates[group] = self._ADDON_SELECTED
        except PayloadError as e:
            # Group translation is not supported
            log.warning(e)
            # It's better to have all or nothing selected from kickstart
            self._addonStates = {}

        if not self._kickstarted:
            # having done all the slow downloading, we need to do the first refresh
            # of the UI here so there's an environment selected by default.  This
            # happens inside the main thread by necessity.  We can't do anything
            # that takes any real amount of time, or it'll block the UI from
            # updating.
            if not self._first_refresh():
                return

        hubQ.send_ready(self.__class__.__name__, False)

        # If packages were provided by an input kickstart file (or some other means),
        # we should do dependency solving here.
        if not self._error:
            self._apply()

        # report that software spoke initialization has been completed
        self.initialize_done()

    @async_action_wait
    def _first_refresh(self):
        self.refresh()
        return True

    def _add_row(self, listbox, name, desc, button, clicked):
        row = Gtk.ListBoxRow()
        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)

        button.set_valign(Gtk.Align.START)
        button.connect("toggled", clicked, row)
        box.add(button)

        label = Gtk.Label(label="<b>%s</b>\n%s" %
                          (escape_markup(name), escape_markup(desc)),
                          use_markup=True,
                          wrap=True,
                          wrap_mode=Pango.WrapMode.WORD_CHAR,
                          hexpand=True,
                          xalign=0,
                          yalign=0.5)
        box.add(label)

        row.add(box)
        listbox.insert(row, -1)

    def refresh(self):
        super().refresh()

        threadMgr.wait(constants.THREAD_PAYLOAD)

        firstEnvironment = True

        self._clear_listbox(self._environmentListBox)

        # If no environment is selected, use the default from the instclass.
        # If nothing is set in the instclass, the first environment will be
        # selected below.
        if not self.environment and self.payload.instclass and \
                self.payload.instclass.defaultPackageEnvironment in self.payload.environments:
            self.environment = self.payload.instclass.defaultPackageEnvironment

        # create rows for all valid environments
        for environmentid in self.payload.environments:
            (name, desc) = self.payload.environmentDescription(environmentid)

            # use the invisible radio button as a group for all environment
            # radio buttons
            radio = Gtk.RadioButton(group=self._fakeRadio)

            # automatically select the first environment if we are on
            # manual install and the install class does not specify one
            if firstEnvironment and not flags.automatedInstall:  # manual installation
                #
                # Note about self.environment being None:
                # =======================================
                # None indicates that an environment has not been set, which is a valid
                # value of the environment variable.
                # Only non existing environments are evaluated as invalid
                if not self.environment_valid or self.environment is None:
                    self.environment = environmentid
                firstEnvironment = False

            # check if the selected environment (if any) does match the current row
            # and tick the radio button if it does
            radio.set_active(self.environment_valid
                             and self.environmentid == environmentid)

            self._add_row(self._environmentListBox, name, desc, radio,
                          self.on_radio_button_toggled)

        self.refreshAddons()
        self._environmentListBox.show_all()
        self._addonListBox.show_all()

    def _addAddon(self, grp):
        (name, desc) = self.payload.groupDescription(grp)

        if grp in self._addonStates:
            # If the add-on was previously selected by the user, select it
            if self._addonStates[grp] == self._ADDON_SELECTED:
                selected = True
            # If the add-on was previously de-selected by the user, de-select it
            elif self._addonStates[grp] == self._ADDON_DESELECTED:
                selected = False
            # Otherwise, use the default state
            else:
                selected = self.payload.environmentOptionIsDefault(
                    self.environmentid, grp)
        else:
            selected = self.payload.environmentOptionIsDefault(
                self.environmentid, grp)

        check = Gtk.CheckButton()
        check.set_active(selected)
        self._add_row(self._addonListBox, name, desc, check,
                      self.on_checkbox_toggled)

    @property
    def _addSep(self):
        """ Whether the addon list contains a separator. """
        return len(self.payload.environmentAddons[self.environmentid][0]) > 0 and \
            len(self.payload.environmentAddons[self.environmentid][1]) > 0

    def refreshAddons(self):
        if self.environment and (self.environmentid
                                 in self.payload.environmentAddons):
            self._clear_listbox(self._addonListBox)

            # We have two lists:  One of addons specific to this environment,
            # and one of all the others.  The environment-specific ones will be displayed
            # first and then a separator, and then the generic ones.  This is to make it
            # a little more obvious that the thing on the left side of the screen and the
            # thing on the right side of the screen are related.
            #
            # If a particular add-on was previously selected or de-selected by the user, that
            # state will be used. Otherwise, the add-on will be selected if it is a default
            # for this environment.

            for grp in self.payload.environmentAddons[self.environmentid][0]:
                self._addAddon(grp)

            # This marks a separator in the view - only add it if there's both environment
            # specific and generic addons.
            if self._addSep:
                self._addonListBox.insert(Gtk.Separator(), -1)

            for grp in self.payload.environmentAddons[self.environmentid][1]:
                self._addAddon(grp)

        self._selectFlag = True

        if self._errorMsgs:
            self.set_warning(
                _("Error checking software dependencies.  <a href=\"\">Click for details.</a>"
                  ))
        else:
            self.clear_info()

    def _allAddons(self):
        if self.environmentid in self.payload.environmentAddons:
            addons = copy.copy(
                self.payload.environmentAddons[self.environmentid][0])
            if self._addSep:
                addons.append('')
            addons += self.payload.environmentAddons[self.environmentid][1]
        else:
            addons = []
        return addons

    def _get_selected_addons(self):
        retval = []
        addons = self._allAddons()

        for (ndx, row) in enumerate(self._addonListBox.get_children()):
            box = row.get_children()[0]

            if isinstance(box, Gtk.Separator):
                continue

            button = box.get_children()[0]
            if button.get_active():
                retval.append(addons[ndx])

        return retval

    def _mark_addon_selection(self, grpid, selected):
        # Mark selection or return its state to the default state
        if selected:
            if self.payload.environmentOptionIsDefault(self.environment,
                                                       grpid):
                self._addonStates[grpid] = self._ADDON_DEFAULT
            else:
                self._addonStates[grpid] = self._ADDON_SELECTED
        else:
            if not self.payload.environmentOptionIsDefault(
                    self.environment, grpid):
                self._addonStates[grpid] = self._ADDON_DEFAULT
            else:
                self._addonStates[grpid] = self._ADDON_DESELECTED

    def _clear_listbox(self, listbox):
        for child in listbox.get_children():
            listbox.remove(child)
            del (child)

    @property
    def txid_valid(self):
        return self._tx_id == self.payload.txID

    # Signal handlers
    def on_radio_button_toggled(self, radio, row):
        # If the radio button toggled to inactive, don't reactivate the row
        if not radio.get_active():
            return
        row.activate()

    def on_environment_activated(self, listbox, row):
        if not self._selectFlag:
            return

        # GUI selections means that packages are no longer coming from kickstart
        self._kickstarted = False

        box = row.get_children()[0]
        button = box.get_children()[0]

        with blockedHandler(button, self.on_radio_button_toggled):
            button.set_active(True)

        # Mark the clicked environment as selected and update the screen.
        self.environment = self.payload.environments[row.get_index()]
        self.refreshAddons()
        self._addonListBox.show_all()

    def on_checkbox_toggled(self, button, row):
        # Select the addon. The button is already toggled.
        self._select_addon_at_row(row, button.get_active())

    def on_addon_activated(self, listbox, row):
        # Skip the separator.
        box = row.get_children()[0]
        if isinstance(box, Gtk.Separator):
            return

        # Select the addon. The button is not toggled yet.
        button = box.get_children()[0]
        self._select_addon_at_row(row, not button.get_active())

    def _select_addon_at_row(self, row, is_selected):
        # GUI selections means that packages are no longer coming from kickstart.
        self._kickstarted = False

        # Activate the row.
        with blockedHandler(row.get_parent(), self.on_addon_activated):
            row.activate()

        # Activate the button.
        box = row.get_children()[0]
        button = box.get_children()[0]
        with blockedHandler(button, self.on_checkbox_toggled):
            button.set_active(is_selected)

        # Mark the selection.
        addons = self._allAddons()
        group = addons[row.get_index()]
        self._mark_addon_selection(group, is_selected)

    def on_info_bar_clicked(self, *args):
        if not self._errorMsgs:
            return

        label = _(
            "The software marked for installation has the following errors.  "
            "This is likely caused by an error with your installation source.  "
            "You can quit the installer, change your software source, or change "
            "your software selections.")
        dialog = DetailedErrorDialog(
            self.data,
            buttons=[
                C_("GUI|Software Selection|Error Dialog", "_Quit"),
                C_("GUI|Software Selection|Error Dialog",
                   "_Modify Software Source"),
                C_("GUI|Software Selection|Error Dialog", "Modify _Selections")
            ],
            label=label)
        with self.main_window.enlightbox(dialog.window):
            dialog.refresh(self._errorMsgs)
            rc = dialog.run()

        dialog.window.destroy()

        if rc == 0:
            # Quit.
            util.ipmi_abort(scripts=self.data.scripts)
            sys.exit(0)
        elif rc == 1:
            # Send the user to the installation source spoke.
            self.skipTo = "SourceSpoke"
            self.window.emit("button-clicked")
        elif rc == 2:
            # Close the dialog so the user can change selections.
            pass
        else:
            pass
Пример #16
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 = "blivet-gui/index.page"

    ### methods defined by API ###
    def __init__(self, data, storage, payload):
        """
        :see: pyanaconda.ui.common.Spoke.__init__
        :param data: data object passed to every spoke to load/store data
                     from/to it
        :type data: pykickstart.base.BaseHandler
        :param storage: object storing storage-related information
                        (disks, partitioning, bootloader, etc.)
        :type storage: blivet.Blivet
        :param payload: object storing payload-related information
        :type payload: pyanaconda.payload.Payload
        """
        self._error = None
        self._back_already_clicked = False
        self._label_actions = None
        self._button_reset = None
        self._button_undo = None

        self._client = None
        self._blivetgui = None
        self._partitioning = None
        self._device_tree = None

        self._storage_module = STORAGE.get_proxy()

        StorageCheckHandler.__init__(self)
        NormalSpoke.__init__(self, data, storage, payload)

    @property
    def label_actions(self):
        """The summary label.

        This property is required by Blivet-GUI.
        """
        return self._label_actions

    def initialize(self):
        """
        The initialize method that is called after the instance is created.
        The difference between __init__ and this method is that this may take
        a long time and thus could be called in a separated thread.

        :see: pyanaconda.ui.common.UIObject.initialize
        """
        NormalSpoke.initialize(self)
        self.initialize_start()

        config.log_dir = "/tmp"

        box = self.builder.get_object("BlivetGuiViewport")
        self._label_actions = self.builder.get_object("summary_label")
        self._button_reset = self.builder.get_object("resetAllButton")
        self._button_undo = self.builder.get_object("undoLastActionButton")

        self._client = BlivetGUIAnacondaClient()
        self._blivetgui = BlivetGUIAnaconda(self._client, self, box)

        # this needs to be done when the spoke is already "realized"
        self.entered.connect(self._blivetgui.ui_refresh)

        # set up keyboard shurtcuts for blivet-gui (and unset them after
        # user lefts the spoke)
        self.entered.connect(self._blivetgui.set_keyboard_shortcuts)
        self.exited.connect(self._blivetgui.unset_keyboard_shortcuts)

        self.initialize_done()

    def refresh(self):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of
        self.data.

        :see: pyanaconda.ui.common.UIObject.refresh
        """
        for thread_name in [THREAD_EXECUTE_STORAGE, THREAD_STORAGE]:
            threadMgr.wait(thread_name)

        if not self._partitioning:
            # Create the partitioning now. It cannot by done earlier, because
            # the storage spoke would use it as a default partitioning.
            self._partitioning = create_partitioning(
                PARTITIONING_METHOD_BLIVET)
            self._device_tree = STORAGE.get_proxy(
                self._partitioning.GetDeviceTree())

        self._back_already_clicked = False
        self._client.initialize(self._partitioning.SendRequest)
        self._blivetgui.initialize()

        # if we re-enter blivet-gui spoke, actions from previous visit were
        # not removed, we need to update number of blivet-gui actions
        self._blivetgui.set_actions(self._client.get_actions())

    def apply(self):
        """
        The apply method that is called when the spoke is left. It should
        update the contents of self.data with values set in the GUI elements.
        """
        pass

    @property
    def indirect(self):
        return True

    # This spoke has no status since it's not in a hub
    @property
    def status(self):
        return None

    def clear_errors(self):
        self._error = None
        self.clear_info()

    def _do_check(self):
        self.clear_errors()
        StorageCheckHandler.errors = []
        StorageCheckHandler.warnings = []

        try:
            log.debug("Generating updated storage configuration")
            task_path = self._partitioning.ConfigureWithTask()
            task_proxy = STORAGE.get_proxy(task_path)
            sync_run_task(task_proxy)
        except BootloaderConfigurationError as e:
            log.error("Storage configuration failed: %s", e)
            StorageCheckHandler.errors = [str(e)]
            reset_bootloader()
        else:
            log.debug("Checking storage configuration...")
            task_path = self._partitioning.ValidateWithTask()
            task_proxy = STORAGE.get_proxy(task_path)
            sync_run_task(task_proxy)

            result = unwrap_variant(task_proxy.GetResult())
            report = ValidationReport.from_structure(result)

            log.debug("Validation has been completed: %s", report)
            StorageCheckHandler.errors = report.error_messages
            StorageCheckHandler.warnings = report.warning_messages

            if report.is_valid():
                self._storage_module.ApplyPartitioning(
                    get_object_path(self._partitioning))

        if self.errors:
            self.set_warning(
                _("Error checking storage configuration.  <a href=\"\">Click for details</a> or press Done again to continue."
                  ))
        elif self.warnings:
            self.set_warning(
                _("Warning checking storage configuration.  <a href=\"\">Click for details</a> or press Done again to continue."
                  ))

        # on_info_bar_clicked requires self._error to be set, so set it to the
        # list of all errors and warnings that storage checking found.
        self._error = "\n".join(self.errors + self.warnings)

        return self._error == ""

    def activate_action_buttons(self, activate):
        self._button_undo.set_sensitive(activate)
        self._button_reset.set_sensitive(activate)

    ### handlers ###
    def on_info_bar_clicked(self, *args):
        log.debug("info bar clicked: %s (%s)", self._error, args)
        if not self._error:
            return

        dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
                                message_type=Gtk.MessageType.ERROR,
                                buttons=Gtk.ButtonsType.CLOSE,
                                message_format=str(self._error))
        dlg.set_decorated(False)

        with self.main_window.enlightbox(dlg):
            dlg.run()
            dlg.destroy()

    def on_back_clicked(self, button):
        # Clear any existing errors
        self.clear_errors()

        # If back has been clicked on once already and no other changes made on the screen,
        # run the storage check now.  This handles displaying any errors in the info bar.
        if not self._back_already_clicked:
            self._back_already_clicked = True

            # If we hit any errors while saving things above, stop and let the
            # user think about what they have done
            if self._error is not None:
                return

            if not self._do_check():
                return

        dialog = ActionSummaryDialog(self.data, self._device_tree)
        dialog.refresh()

        if dialog.actions:
            with self.main_window.enlightbox(dialog.window):
                rc = dialog.run()

            if rc != 1:
                # Cancel.  Stay on the blivet-gui screen.
                return

        NormalSpoke.on_back_clicked(self, button)

    def on_summary_button_clicked(self, _button):
        self._blivetgui.show_actions()

    def on_undo_action_button_clicked(self, _button):
        self._blivetgui.actions_undo()

    # This callback is for the button that just resets the UI to anaconda's
    # current understanding of the disk layout.
    def on_reset_button_clicked(self, *args):
        msg = _(
            "Continuing with this action will reset all your partitioning selections "
            "to their current on-disk state.")

        dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
                                message_type=Gtk.MessageType.WARNING,
                                buttons=Gtk.ButtonsType.NONE,
                                message_format=msg)
        dlg.set_decorated(False)
        dlg.add_buttons(
            C_("GUI|Custom Partitioning|Reset Dialog", "_Reset selections"), 0,
            C_("GUI|Custom Partitioning|Reset Dialog",
               "_Preserve current selections"), 1)
        dlg.set_default_response(1)

        with self.main_window.enlightbox(dlg):
            rc = dlg.run()
            dlg.destroy()

        if rc == 0:
            self.refresh()
            self._blivetgui.reload()

            # XXX: Reset currently preserves actions set in previous runs
            # of the spoke, so we need to 're-add' these to the ui
            self._blivetgui.set_actions(self._client.get_actions())
Пример #17
0
class SoftwareSelectionSpoke(NormalSpoke):
    """
       .. inheritance-diagram:: SoftwareSelectionSpoke
          :parts: 3
    """
    builderObjects = ["addonStore", "environmentStore", "softwareWindow"]
    mainWidgetName = "softwareWindow"
    uiFile = "spokes/software_selection.glade"
    helpFile = "SoftwareSpoke.xml"

    category = SoftwareCategory

    icon = "package-x-generic-symbolic"
    title = CN_("GUI|Spoke", "_Software Selection")

    # Add-on selection states
    # no user interaction with this add-on
    _ADDON_DEFAULT = 0
    # user selected
    _ADDON_SELECTED = 1
    # user de-selected
    _ADDON_DESELECTED = 2

    @classmethod
    def should_run(cls, environment, data):
        """Don't run for any non-package payload."""
        if not NormalSpoke.should_run(environment, data):
            return False

        return context.payload_type == PAYLOAD_TYPE_DNF

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._error_msgs = None
        self._tx_id = None
        self._select_flag = False

        self._environment_list_box = self.builder.get_object("environmentListBox")
        self._addon_list_box = self.builder.get_object("addonListBox")

        # Connect viewport scrolling with listbox focus events
        environment_viewport = self.builder.get_object("environmentViewport")
        addon_viewport = self.builder.get_object("addonViewport")
        self._environment_list_box.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(environment_viewport))
        self._addon_list_box.set_focus_vadjustment(
            Gtk.Scrollable.get_vadjustment(addon_viewport))

        # Used to store how the user has interacted with add-ons for the default add-on
        # selection logic. The dictionary keys are group IDs, and the values are selection
        # state constants. See refresh_addons for how the values are used.
        self._addon_states = {}

        # Get the packages configuration.
        self._selection = self.payload.get_packages_data()

        # Whether we are using package selections from a kickstart
        self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted

        # Whether the payload is in an error state
        self._error = False

        # Register event listeners to update our status on payload events
        payloadMgr.add_listener(PayloadState.DOWNLOADING_PKG_METADATA,
                                self._downloading_package_md)
        payloadMgr.add_listener(PayloadState.DOWNLOADING_GROUP_METADATA,
                                self._downloading_group_md)
        payloadMgr.add_listener(PayloadState.ERROR, self._payload_error)

        # Add an invisible radio button so that we can show the environment
        # list with no radio buttons ticked
        self._fake_radio = Gtk.RadioButton(group=None)
        self._fake_radio.set_active(True)

    # Payload event handlers
    def _downloading_package_md(self):
        # Reset the error state from previous payloads
        self._error = False

        hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_PACKAGE_MD))

    def _downloading_group_md(self):
        hubQ.send_message(self.__class__.__name__, _(constants.PAYLOAD_STATUS_GROUP_MD))

    def get_environment_id(self, environment):
        """Return the "machine readable" environment id

        Alternatively we could have just "canonicalized" the
        environment description to the "machine readable" format
        when reading it from kickstart for the first time.
        But this could result in input and output kickstart,
        which would be rather confusing for the user.
        So we don't touch the specification from kickstart
        if it is valid and use this property when we need
        the "machine readable" form.
        """
        if not environment:
            # None means environment is not set, no need to try translate that to an id
            return None
        try:
            return self.payload.environment_id(environment)
        except NoSuchGroup:
            return None

    def is_environment_valid(self, environment):
        """Return if the currently set environment is valid
        (represents an environment known by the payload)
        """
        # None means the environment has not been set by the user,
        # which means:
        # * set the default environment during interactive installation
        # * ask user to specify an environment during kickstart installation
        if not environment:
            return True
        else:
            return self.get_environment_id(environment) in self.payload.environments

    def _payload_error(self):
        self._error = True
        hubQ.send_message(self.__class__.__name__, payloadMgr.error)

    def apply(self):
        """Apply the changes."""
        self._kickstarted = False

        # Clear packages data.
        self._selection.packages = []
        self._selection.excluded_packages = []

        # Clear groups data.
        self._selection.excluded_groups = []
        self._selection.groups_package_types = {}

        # Select new groups.
        self._selection.groups = []

        for group_name in self._get_selected_addons():
            self._selection.groups.append(group_name)

        log.debug("Setting new software selection: %s", self._selection)
        self.payload.set_packages_data(self._selection)

        hubQ.send_not_ready(self.__class__.__name__)
        hubQ.send_not_ready("SourceSpoke")

    def execute(self):
        """Execute the changes."""
        threadMgr.add(AnacondaThread(
            name=constants.THREAD_CHECK_SOFTWARE,
            target=self.checkSoftwareSelection
        ))

    def checkSoftwareSelection(self):
        hubQ.send_message(self.__class__.__name__, _("Checking software dependencies..."))
        try:
            self.payload.check_software_selection()
        except DependencyError as e:
            self._error_msgs = str(e)
            hubQ.send_message(self.__class__.__name__, _("Error checking software dependencies"))
            self._tx_id = None
        else:
            self._error_msgs = None
            self._tx_id = self.payload.tx_id
        finally:
            hubQ.send_ready(self.__class__.__name__)
            hubQ.send_ready("SourceSpoke")

    @property
    def completed(self):
        processing_done = bool(not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and
                               not threadMgr.get(constants.THREAD_PAYLOAD) and
                               not self._error_msgs and self.txid_valid)

        # * we should always check processing_done before checking the other variables,
        #   as they might be inconsistent until processing is finished
        # * we can't let the installation proceed until a valid environment has been set
        if processing_done:
            if self._selection.environment:
                # if we have environment it needs to be valid
                return self.is_environment_valid(self._selection.environment)
            # if we don't have environment we need to at least have the %packages
            # section in kickstart
            elif self._kickstarted:
                return True
            # no environment and no %packages section -> manual intervention is needed
            else:
                return False
        else:
            return False

    @property
    def mandatory(self):
        return True

    @property
    def ready(self):
        """Is the spoke ready?

        By default, the software selection spoke is not ready. We have to
        wait until the installation source spoke is completed. This could be
        because the user filled something out, or because we're done fetching
        repo metadata from the mirror list, or we detected a DVD/CD.
        """
        return not threadMgr.get(THREAD_SOFTWARE_WATCHER) \
            and not threadMgr.get(THREAD_PAYLOAD) \
            and not threadMgr.get(THREAD_CHECK_SOFTWARE) \
            and self.payload.base_repo is not None

    @property
    def status(self):
        if self._error_msgs:
            return _("Error checking software selection")

        cdn_source = check_cdn_is_installation_source(self.payload)

        subscribed = False
        if is_module_available(SUBSCRIPTION):
            subscription_proxy = SUBSCRIPTION.get_proxy()
            subscribed = subscription_proxy.IsSubscriptionAttached

        if cdn_source and not subscribed:
            return _("Red Hat CDN requires registration.")

        if not self.ready:
            return _("Installation source not set up")

        if not self.txid_valid:
            return _("Source changed - please verify")

        # kickstart installation
        if flags.automatedInstall:
            if self._kickstarted:
                # %packages section is present in kickstart but environment is not set
                if not self._selection.environment:
                    return _("Custom software selected")
                # environment is set to an invalid value
                elif not self.is_environment_valid(self._selection.environment):
                    return _("Invalid environment specified in kickstart")
            # we have no packages section in the kickstart and no environment has been set
            elif not self._selection.environment:
                return _("Please confirm software selection")

        if not flags.automatedInstall:
            if not self._selection.environment:
                # No environment yet set
                return _("Please confirm software selection")
            elif not self.is_environment_valid(self._selection.environment):
                # selected environment is not valid, this can happen when a valid environment
                # is selected (by default, manually or from kickstart) and then the installation
                # source is switched to one where the selected environment is no longer valid
                return _("Selected environment is not valid")

        return self.payload.environment_description(self._selection.environment)[0]

    def initialize(self):
        """Initialize the spoke."""
        super().initialize()
        self.initialize_start()

        threadMgr.add(AnacondaThread(
            name=constants.THREAD_SOFTWARE_WATCHER,
            target=self._initialize
        ))

    def _initialize(self):
        """Initialize the spoke in a separate thread."""
        threadMgr.wait(constants.THREAD_PAYLOAD)

        # Initialize and check the software selection.
        self._initialize_selection()

        # Update the status.
        hubQ.send_ready(self.__class__.__name__)

        # Report that the software spoke has been initialized.
        self.initialize_done()

    def _initialize_selection(self):
        """Initialize and check the software selection."""
        if self._error or not self.payload.base_repo:
            log.debug("Skip the initialization of the software selection.")
            return

        if not self._kickstarted:
            # Set the environment.
            self.set_default_environment()

            # Apply the initial selection.
            self.apply()

        # Check the initial software selection.
        self.execute()

        # Wait for the software selection thread that might be started by execute().
        # We are already running in a thread, so it should not needlessly block anything
        # and only like this we can be sure we are really initialized.
        threadMgr.wait(constants.THREAD_CHECK_SOFTWARE)

    def set_default_environment(self):
        # If an environment was specified in the configuration, use that.
        # Otherwise, select the first environment.
        if self.payload.environments:
            environments = self.payload.environments

            if conf.payload.default_environment in environments:
                self._selection.environment = conf.payload.default_environment
            else:
                self._selection.environment = environments[0]

    def _add_row(self, listbox, name, desc, button, clicked):
        row = Gtk.ListBoxRow()
        box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)

        button.set_valign(Gtk.Align.START)
        button.connect("toggled", clicked, row)
        box.add(button)

        label = Gtk.Label(label="<b>%s</b>\n%s" % (escape_markup(name), escape_markup(desc)),
                          use_markup=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR,
                          hexpand=True, xalign=0, yalign=0.5)
        box.add(label)

        row.add(box)
        listbox.insert(row, -1)

    def refresh(self):
        super().refresh()
        threadMgr.wait(constants.THREAD_PAYLOAD)

        # Get the packages configuration.
        self._selection = self.payload.get_packages_data()

        # Set up the environment.
        if not self._selection.environment \
                or not self.is_environment_valid(self._selection.environment):
            self.set_default_environment()

        # Create rows for all valid environments.
        self._clear_listbox(self._environment_list_box)

        for environment_id in self.payload.environments:
            (name, desc) = self.payload.environment_description(environment_id)

            # use the invisible radio button as a group for all environment
            # radio buttons
            radio = Gtk.RadioButton(group=self._fake_radio)

            # check if the selected environment (if any) does match the current row
            # and tick the radio button if it does
            radio.set_active(
                self.is_environment_valid(self._selection.environment) and
                self.get_environment_id(self._selection.environment) == environment_id
            )

            self._add_row(self._environment_list_box,
                          name, desc, radio,
                          self.on_radio_button_toggled)

        # Set up states of selected groups.
        self._addon_states = {}

        for group in self._selection.groups:
            try:
                group_id = self.payload.group_id(group)
                self._mark_addon_selection(group_id, True)
            except PayloadError as e:
                log.warning(e)

        self.refresh_addons()
        self._environment_list_box.show_all()
        self._addon_list_box.show_all()

    def _add_addon(self, grp):
        (name, desc) = self.payload.group_description(grp)

        if grp in self._addon_states:
            # If the add-on was previously selected by the user, select it
            if self._addon_states[grp] == self._ADDON_SELECTED:
                selected = True
            # If the add-on was previously de-selected by the user, de-select it
            elif self._addon_states[grp] == self._ADDON_DESELECTED:
                selected = False
            # Otherwise, use the default state
            else:
                selected = self.payload.environment_option_is_default(
                    self.get_environment_id(self._selection.environment), grp
                )
        else:
            selected = self.payload.environment_option_is_default(
                self.get_environment_id(self._selection.environment), grp
            )

        check = Gtk.CheckButton()
        check.set_active(selected)
        self._add_row(self._addon_list_box, name, desc, check, self.on_checkbox_toggled)

    @property
    def _add_sep(self):
        """ Whether the addon list contains a separator. """
        environment_id = self.get_environment_id(self._selection.environment)

        return len(self.payload.environment_addons[environment_id][0]) > 0 and \
            len(self.payload.environment_addons[environment_id][1]) > 0

    def refresh_addons(self):
        environment = self._selection.environment
        environment_id = self.get_environment_id(self._selection.environment)

        if environment and (environment_id in self.payload.environment_addons):
            self._clear_listbox(self._addon_list_box)

            # We have two lists:  One of addons specific to this environment,
            # and one of all the others.  The environment-specific ones will be displayed
            # first and then a separator, and then the generic ones.  This is to make it
            # a little more obvious that the thing on the left side of the screen and the
            # thing on the right side of the screen are related.
            #
            # If a particular add-on was previously selected or de-selected by the user, that
            # state will be used. Otherwise, the add-on will be selected if it is a default
            # for this environment.

            for grp in self.payload.environment_addons[environment_id][0]:
                self._add_addon(grp)

            # This marks a separator in the view - only add it if there's both environment
            # specific and generic addons.
            if self._add_sep:
                self._addon_list_box.insert(Gtk.Separator(), -1)

            for grp in self.payload.environment_addons[environment_id][1]:
                self._add_addon(grp)

        self._select_flag = True

        if self._error_msgs:
            self.set_warning(_("Error checking software dependencies. "
                               " <a href=\"\">Click for details.</a>"))
        else:
            self.clear_info()

    def _all_addons(self):
        environment_id = self.get_environment_id(self._selection.environment)

        if environment_id in self.payload.environment_addons:
            addons = copy.copy(self.payload.environment_addons[environment_id][0])

            if self._add_sep:
                addons.append('')

            addons += self.payload.environment_addons[environment_id][1]
        else:
            addons = []

        return addons

    def _get_selected_addons(self):
        retval = []
        addons = self._all_addons()

        for (ndx, row) in enumerate(self._addon_list_box.get_children()):
            box = row.get_children()[0]

            if isinstance(box, Gtk.Separator):
                continue

            button = box.get_children()[0]
            if button.get_active():
                retval.append(addons[ndx])

        return retval

    def _mark_addon_selection(self, grpid, selected):
        # Mark selection or return its state to the default state
        if selected:
            if self.payload.environment_option_is_default(self._selection.environment, grpid):
                self._addon_states[grpid] = self._ADDON_DEFAULT
            else:
                self._addon_states[grpid] = self._ADDON_SELECTED
        else:
            if not self.payload.environment_option_is_default(self._selection.environment, grpid):
                self._addon_states[grpid] = self._ADDON_DEFAULT
            else:
                self._addon_states[grpid] = self._ADDON_DESELECTED

    def _clear_listbox(self, listbox):
        for child in listbox.get_children():
            listbox.remove(child)
            del(child)

    @property
    def txid_valid(self):
        return self._tx_id == self.payload.tx_id

    # Signal handlers
    def on_radio_button_toggled(self, radio, row):
        # If the radio button toggled to inactive, don't reactivate the row
        if not radio.get_active():
            return
        row.activate()

    def on_environment_activated(self, listbox, row):
        if not self._select_flag:
            return

        # GUI selections means that packages are no longer coming from kickstart
        self._kickstarted = False

        box = row.get_children()[0]
        button = box.get_children()[0]

        with blockedHandler(button, self.on_radio_button_toggled):
            button.set_active(True)

        # Mark the clicked environment as selected and update the screen.
        self._selection.environment = self.payload.environments[row.get_index()]
        self.refresh_addons()
        self._addon_list_box.show_all()

    def on_checkbox_toggled(self, button, row):
        # Select the addon. The button is already toggled.
        self._select_addon_at_row(row, button.get_active())

    def on_addon_activated(self, listbox, row):
        # Skip the separator.
        box = row.get_children()[0]
        if isinstance(box, Gtk.Separator):
            return

        # Select the addon. The button is not toggled yet.
        button = box.get_children()[0]
        self._select_addon_at_row(row, not button.get_active())

    def _select_addon_at_row(self, row, is_selected):
        # GUI selections means that packages are no longer coming from kickstart.
        self._kickstarted = False

        # Activate the row.
        with blockedHandler(row.get_parent(), self.on_addon_activated):
            row.activate()

        # Activate the button.
        box = row.get_children()[0]
        button = box.get_children()[0]
        with blockedHandler(button, self.on_checkbox_toggled):
            button.set_active(is_selected)

        # Mark the selection.
        addons = self._all_addons()
        group = addons[row.get_index()]
        self._mark_addon_selection(group, is_selected)

    def on_info_bar_clicked(self, *args):
        if not self._error_msgs:
            return

        label = _("The software marked for installation has the following errors.  "
                  "This is likely caused by an error with your installation source.  "
                  "You can quit the installer, change your software source, or change "
                  "your software selections.")
        dialog = DetailedErrorDialog(
            self.data,
            buttons=[C_("GUI|Software Selection|Error Dialog", "_Quit"),
                     C_("GUI|Software Selection|Error Dialog", "_Modify Software Source"),
                     C_("GUI|Software Selection|Error Dialog", "Modify _Selections")],
            label=label)
        with self.main_window.enlightbox(dialog.window):
            dialog.refresh(self._error_msgs)
            rc = dialog.run()

        dialog.window.destroy()

        if rc == 0:
            # Quit.
            util.ipmi_abort(scripts=self.data.scripts)
            sys.exit(0)
        elif rc == 1:
            # Send the user to the installation source spoke.
            self.skipTo = "SourceSpoke"
            self.window.emit("button-clicked")
        elif rc == 2:
            # Close the dialog so the user can change selections.
            pass
        else:
            pass
Пример #18
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 = "blivet-gui/index.page"

    ### methods defined by API ###
    def __init__(self, data, storage, payload, instclass):
        """
        :see: pyanaconda.ui.common.Spoke.__init__
        :param data: data object passed to every spoke to load/store data
                     from/to it
        :type data: pykickstart.base.BaseHandler
        :param storage: object storing storage-related information
                        (disks, partitioning, bootloader, etc.)
        :type storage: blivet.Blivet
        :param payload: object storing payload-related information
        :type payload: pyanaconda.payload.Payload
        :param instclass: distribution-specific information
        :type instclass: pyanaconda.installclass.BaseInstallClass

        """

        self._error = None
        self._back_already_clicked = False
        self._storage_playground = None
        self.label_actions = None
        self.button_reset = None
        self.button_undo = None

        self._bootloader_observer = STORAGE.get_observer(BOOTLOADER)
        self._bootloader_observer.connect()

        StorageCheckHandler.__init__(self)
        NormalSpoke.__init__(self, data, storage, payload, instclass)

    def initialize(self):
        """
        The initialize method that is called after the instance is created.
        The difference between __init__ and this method is that this may take
        a long time and thus could be called in a separated thread.

        :see: pyanaconda.ui.common.UIObject.initialize

        """

        NormalSpoke.initialize(self)
        self.initialize_start()

        self._storage_playground = None

        config.log_dir = "/tmp"
        self.client = osinstall.BlivetGUIAnacondaClient()
        box = self.builder.get_object("BlivetGuiViewport")
        self.label_actions = self.builder.get_object("summary_label")
        self.button_reset = self.builder.get_object("resetAllButton")
        self.button_undo = self.builder.get_object("undoLastActionButton")

        config.default_fstype = self._storage.default_fstype

        self.blivetgui = osinstall.BlivetGUIAnaconda(self.client, self, box)

        # this needs to be done when the spoke is already "realized"
        self.entered.connect(self.blivetgui.ui_refresh)

        # set up keyboard shurtcuts for blivet-gui (and unset them after
        # user lefts the spoke)
        self.entered.connect(self.blivetgui.set_keyboard_shortcuts)
        self.exited.connect(self.blivetgui.unset_keyboard_shortcuts)

        self.initialize_done()

    def refresh(self):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of
        self.data.

        :see: pyanaconda.ui.common.UIObject.refresh

        """

        self._back_already_clicked = False

        self._storage_playground = self.storage.copy()
        self.client.initialize(self._storage_playground)
        self.blivetgui.initialize()

        # if we re-enter blivet-gui spoke, actions from previous visit were
        # not removed, we need to update number of blivet-gui actions
        current_actions = self._storage_playground.devicetree.actions.find()
        if current_actions:
            self.blivetgui.set_actions(current_actions)

    def apply(self):
        """
        The apply method that is called when the spoke is left. It should
        update the contents of self.data with values set in the GUI elements.

        """

        self._set_new_swaps()

    @property
    def indirect(self):
        return True

    # This spoke has no status since it's not in a hub
    @property
    def status(self):
        return None

    def clear_errors(self):
        self._error = None
        self.clear_info()

    def _do_check(self):
        self.clear_errors()
        StorageCheckHandler.errors = []
        StorageCheckHandler.warnings = []

        # We can't overwrite the main Storage instance because all the other
        # spokes have references to it that would get invalidated, but we can
        # achieve the same effect by updating/replacing a few key attributes.
        self.storage.devicetree._devices = self._storage_playground.devicetree._devices
        self.storage.devicetree._actions = self._storage_playground.devicetree._actions
        self.storage.devicetree._hidden = self._storage_playground.devicetree._hidden
        self.storage.devicetree.names = self._storage_playground.devicetree.names
        self.storage.roots = self._storage_playground.roots

        # set up bootloader and check the configuration
        try:
            self.storage.set_up_bootloader()
        except BootLoaderError as e:
            log.error("storage configuration failed: %s", e)
            StorageCheckHandler.errors = str(e).split("\n")
            self._bootloader_observer.proxy.SetDrive(BOOTLOADER_DRIVE_UNSET)

        StorageCheckHandler.checkStorage(self)

        if self.errors:
            self.set_warning(
                _("Error checking storage configuration.  <a href=\"\">Click for details</a> or press Done again to continue."
                  ))
        elif self.warnings:
            self.set_warning(
                _("Warning checking storage configuration.  <a href=\"\">Click for details</a> or press Done again to continue."
                  ))

        # on_info_bar_clicked requires self._error to be set, so set it to the
        # list of all errors and warnings that storage checking found.
        self._error = "\n".join(self.errors + self.warnings)

        return self._error == ""

    def activate_action_buttons(self, activate):
        self.button_undo.set_sensitive(activate)
        self.button_reset.set_sensitive(activate)

    def _set_new_swaps(self):
        new_swaps = [
            d for d in self._storage_playground.devices
            if d.direct and not d.format.exists and not d.partitioned
            and d.format.type == "swap"
        ]

        self.storage.set_fstab_swaps(new_swaps)

    ### handlers ###
    def on_info_bar_clicked(self, *args):
        log.debug("info bar clicked: %s (%s)", self._error, args)
        if not self._error:
            return

        dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
                                message_type=Gtk.MessageType.ERROR,
                                buttons=Gtk.ButtonsType.CLOSE,
                                message_format=str(self._error))
        dlg.set_decorated(False)

        with self.main_window.enlightbox(dlg):
            dlg.run()
            dlg.destroy()

    def on_back_clicked(self, button):
        # Clear any existing errors
        self.clear_errors()

        # If back has been clicked on once already and no other changes made on the screen,
        # run the storage check now.  This handles displaying any errors in the info bar.
        if not self._back_already_clicked:
            self._back_already_clicked = True

            # If we hit any errors while saving things above, stop and let the
            # user think about what they have done
            if self._error is not None:
                return

            if not self._do_check():
                return

        if len(self._storage_playground.devicetree.actions.find()) > 0:
            dialog = ActionSummaryDialog(self.data)
            dialog.refresh(self._storage_playground.devicetree.actions.find())
            with self.main_window.enlightbox(dialog.window):
                rc = dialog.run()

            if rc != 1:
                # Cancel.  Stay on the blivet-gui screen.
                return
            else:
                # remove redundant actions and sort them now
                self._storage_playground.devicetree.actions.prune()
                self._storage_playground.devicetree.actions.sort()

        NormalSpoke.on_back_clicked(self, button)

    def on_summary_button_clicked(self, _button):
        self.blivetgui.show_actions()

    def on_undo_action_button_clicked(self, _button):
        self.blivetgui.actions_undo()

    # This callback is for the button that just resets the UI to anaconda's
    # current understanding of the disk layout.
    def on_reset_button_clicked(self, *args):
        msg = _(
            "Continuing with this action will reset all your partitioning selections "
            "to their current on-disk state.")

        dlg = Gtk.MessageDialog(flags=Gtk.DialogFlags.MODAL,
                                message_type=Gtk.MessageType.WARNING,
                                buttons=Gtk.ButtonsType.NONE,
                                message_format=msg)
        dlg.set_decorated(False)
        dlg.add_buttons(
            C_("GUI|Custom Partitioning|Reset Dialog", "_Reset selections"), 0,
            C_("GUI|Custom Partitioning|Reset Dialog",
               "_Preserve current selections"), 1)
        dlg.set_default_response(1)

        with self.main_window.enlightbox(dlg):
            rc = dlg.run()
            dlg.destroy()

        if rc == 0:
            self.refresh()
            self.blivetgui.reload()

            # XXX: Reset currently preserves actions set in previous runs
            # of the spoke, so we need to 're-add' these to the ui
            current_actions = self._storage_playground.devicetree.actions.find(
            )
            if current_actions:
                self.blivetgui.set_actions(current_actions)
Пример #19
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 = None
        self._start_updating_timer = None
        self._shown = False
        self._tz = None

        self._timezone_module = TIMEZONE.get_observer()
        self._timezone_module.connect()
        self._network_module = NETWORK.get_observer()
        self._network_module.connect()

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()
        self._daysStore = self.builder.get_object("days")
        self._monthsStore = self.builder.get_object("months")
        self._yearsStore = self.builder.get_object("years")
        self._regionsStore = self.builder.get_object("regions")
        self._citiesStore = self.builder.get_object("cities")
        self._tzmap = self.builder.get_object("tzmap")
        self._dateBox = self.builder.get_object("dateBox")

        # we need to know it the new value is the same as previous or not
        self._old_region = None
        self._old_city = None

        self._regionCombo = self.builder.get_object("regionCombobox")
        self._cityCombo = self.builder.get_object("cityCombobox")

        self._daysFilter = self.builder.get_object("daysFilter")
        self._daysFilter.set_visible_func(self.existing_date, None)

        self._citiesFilter = self.builder.get_object("citiesFilter")
        self._citiesFilter.set_visible_func(self.city_in_region, None)

        self._hoursLabel = self.builder.get_object("hoursLabel")
        self._minutesLabel = self.builder.get_object("minutesLabel")
        self._amPmUp = self.builder.get_object("amPmUpButton")
        self._amPmDown = self.builder.get_object("amPmDownButton")
        self._amPmLabel = self.builder.get_object("amPmLabel")
        self._radioButton24h = self.builder.get_object("timeFormatRB")
        self._amPmRevealer = self.builder.get_object("amPmRevealer")

        # Set the entry completions.
        # The text_column property needs to be set here. If we set
        # it in the glade file, the completion doesn't show text.
        region_completion = self.builder.get_object("regionCompletion")
        region_completion.set_text_column(0)

        city_completion = self.builder.get_object("cityCompletion")
        city_completion.set_text_column(0)

        # create widgets for displaying/configuring date
        day_box, self._dayCombo, day_label = _new_date_field_box(
            self._daysFilter)
        self._dayCombo.connect("changed", self.on_day_changed)
        month_box, self._monthCombo, month_label = _new_date_field_box(
            self._monthsStore)
        self._monthCombo.connect("changed", self.on_month_changed)
        year_box, self._yearCombo, year_label = _new_date_field_box(
            self._yearsStore)
        self._yearCombo.connect("changed", self.on_year_changed)

        # get the right order for date widgets and respective formats and put
        # widgets in place
        widgets, formats = resolve_date_format(year_box, month_box, day_box)
        for widget in widgets:
            self._dateBox.pack_start(widget, False, False, 0)

        self._day_format, suffix = formats[widgets.index(day_box)]
        day_label.set_text(suffix)
        self._month_format, suffix = formats[widgets.index(month_box)]
        month_label.set_text(suffix)
        self._year_format, suffix = formats[widgets.index(year_box)]
        year_label.set_text(suffix)

        self._ntpSwitch = self.builder.get_object("networkTimeSwitch")

        self._regions_zones = get_all_regions_and_timezones()

        # Set the initial sensitivity of the AM/PM toggle based on the time-type selected
        self._radioButton24h.emit("toggled")

        if not conf.system.can_set_system_clock:
            self._set_date_time_setting_sensitive(False)

        self._config_dialog = NTPconfigDialog(self.data, self._timezone_module)
        self._config_dialog.initialize()

        threadMgr.add(
            AnacondaThread(name=constants.THREAD_DATE_TIME,
                           target=self._initialize))

    def _initialize(self):
        # a bit hacky way, but should return the translated strings
        for i in range(1, 32):
            day = datetime.date(2000, 1, i).strftime(self._day_format)
            self.add_to_store_idx(self._daysStore, i, day)

        for i in range(1, 13):
            month = datetime.date(2000, i, 1).strftime(self._month_format)
            self.add_to_store_idx(self._monthsStore, i, month)

        for i in range(1990, 2051):
            year = datetime.date(i, 1, 1).strftime(self._year_format)
            self.add_to_store_idx(self._yearsStore, i, year)

        cities = set()
        xlated_regions = ((region, get_xlated_timezone(region))
                          for region in self._regions_zones.keys())
        for region, xlated in sorted(
                xlated_regions, key=functools.cmp_to_key(_compare_regions)):
            self.add_to_store_xlated(self._regionsStore, region, xlated)
            for city in self._regions_zones[region]:
                cities.add((city, get_xlated_timezone(city)))

        for city, xlated in sorted(cities,
                                   key=functools.cmp_to_key(_compare_cities)):
            self.add_to_store_xlated(self._citiesStore, city, xlated)

        self._update_datetime_timer = None
        kickstart_timezone = self._timezone_module.proxy.Timezone
        if is_valid_timezone(kickstart_timezone):
            self._set_timezone(kickstart_timezone)
        elif not flags.flags.automatedInstall:
            log.warning(
                "%s is not a valid timezone, falling back to default (%s)",
                kickstart_timezone, DEFAULT_TZ)
            self._set_timezone(DEFAULT_TZ)
            self._timezone_module.proxy.SetTimezone(DEFAULT_TZ)

        time_init_thread = threadMgr.get(constants.THREAD_TIME_INIT)
        if time_init_thread is not None:
            hubQ.send_message(self.__class__.__name__,
                              _("Restoring hardware time..."))
            threadMgr.wait(constants.THREAD_TIME_INIT)

        hubQ.send_ready(self.__class__.__name__, False)

        # report that we are done
        self.initialize_done()

    @property
    def status(self):
        kickstart_timezone = self._timezone_module.proxy.Timezone

        if kickstart_timezone:
            if is_valid_timezone(kickstart_timezone):
                return _("%s timezone") % get_xlated_timezone(
                    kickstart_timezone)
            else:
                return _("Invalid timezone")
        else:
            location = self._tzmap.get_location()
            if location and location.get_property("zone"):
                return _("%s timezone") % get_xlated_timezone(
                    location.get_property("zone"))
            else:
                return _("Nothing selected")

    def apply(self):
        self._shown = False

        # we could use self._tzmap.get_timezone() here, but it returns "" if
        # Etc/XXXXXX timezone is selected
        region = self._get_active_region()
        city = self._get_active_city()
        # nothing selected, just leave the spoke and
        # return to hub without changing anything
        if not region or not city:
            return

        old_tz = self._timezone_module.proxy.Timezone
        new_tz = region + "/" + city

        self._timezone_module.proxy.SetTimezone(new_tz)

        if old_tz != new_tz:
            # new values, not from kickstart
            # TODO: seen should be set from the module
            self._kickstarted = False

        self._timezone_module.proxy.SetNTPEnabled(self._ntpSwitch.get_active())

    def execute(self):
        if self._update_datetime_timer is not None:
            self._update_datetime_timer.cancel()
        self._update_datetime_timer = None

    @property
    def ready(self):
        return not threadMgr.get("AnaDateTimeThread")

    @property
    def completed(self):
        if self._kickstarted and not self._timezone_module.proxy.Kickstarted:
            # taking values from kickstart, but not specified
            return False
        else:
            return is_valid_timezone(self._timezone_module.proxy.Timezone)

    @property
    def mandatory(self):
        return True

    def refresh(self):
        self._shown = True

        # update the displayed time
        self._update_datetime_timer = Timer()
        self._update_datetime_timer.timeout_sec(1, self._update_datetime)
        self._start_updating_timer = None

        kickstart_timezone = self._timezone_module.proxy.Timezone

        if is_valid_timezone(kickstart_timezone):
            self._tzmap.set_timezone(kickstart_timezone)
            time.tzset()

        self._update_datetime()

        has_active_network = self._network_module.proxy.Connected
        if not has_active_network:
            self._show_no_network_warning()
        else:
            self.clear_info()
            gtk_call_once(self._config_dialog.refresh_servers_state)

        if conf.system.can_set_time_synchronization:
            ntp_working = has_active_network and util.service_running(
                NTP_SERVICE)
        else:
            ntp_working = self._timezone_module.proxy.NTPEnabled

        self._ntpSwitch.set_active(ntp_working)

    @async_action_wait
    def _set_timezone(self, timezone):
        """
        Sets timezone to the city/region comboboxes and the timezone map.

        :param timezone: timezone to set
        :type timezone: str
        :return: if successfully set or not
        :rtype: bool

        """

        parts = timezone.split("/", 1)
        if len(parts) != 2:
            # invalid timezone cannot be set
            return False

        region, city = parts
        self._set_combo_selection(self._regionCombo, region)
        self._set_combo_selection(self._cityCombo, city)

        return True

    @async_action_nowait
    def add_to_store_xlated(self, store, item, xlated):
        store.append([item, xlated])

    @async_action_nowait
    def add_to_store_idx(self, store, idx, item):
        store.append([idx, item])

    def existing_date(self, days_model, days_iter, user_data=None):
        if not days_iter:
            return False
        day = days_model[days_iter][0]

        #days 1-28 are in every month every year
        if day < 29:
            return True

        months_model = self._monthCombo.get_model()
        months_iter = self._monthCombo.get_active_iter()
        if not months_iter:
            return True

        years_model = self._yearCombo.get_model()
        years_iter = self._yearCombo.get_active_iter()
        if not years_iter:
            return True

        try:
            datetime.date(years_model[years_iter][0],
                          months_model[months_iter][0], day)
            return True
        except ValueError:
            return False

    def _get_active_city(self):
        cities_model = self._cityCombo.get_model()
        cities_iter = self._cityCombo.get_active_iter()
        if not cities_iter:
            return None

        return cities_model[cities_iter][0]

    def _get_active_region(self):
        regions_model = self._regionCombo.get_model()
        regions_iter = self._regionCombo.get_active_iter()
        if not regions_iter:
            return None

        return regions_model[regions_iter][0]

    def city_in_region(self, model, itr, user_data=None):
        if not itr:
            return False
        city = model[itr][0]

        region = self._get_active_region()
        if not region:
            return False

        return city in self._regions_zones[region]

    def _set_amPm_part_sensitive(self, sensitive):

        for widget in (self._amPmUp, self._amPmDown, self._amPmLabel):
            widget.set_sensitive(sensitive)

    def _to_amPm(self, hours):
        if hours >= 12:
            day_phase = _("PM")
        else:
            day_phase = _("AM")

        new_hours = ((hours - 1) % 12) + 1

        return (new_hours, day_phase)

    def _to_24h(self, hours, day_phase):
        correction = 0

        if day_phase == _("AM") and hours == 12:
            correction = -12

        elif day_phase == _("PM") and hours != 12:
            correction = 12

        return (hours + correction) % 24

    def _update_datetime(self):
        now = datetime.datetime.now(self._tz)
        if self._radioButton24h.get_active():
            self._hoursLabel.set_text("%0.2d" % now.hour)
        else:
            hours, amPm = self._to_amPm(now.hour)
            self._hoursLabel.set_text("%0.2d" % hours)
            self._amPmLabel.set_text(amPm)

        self._minutesLabel.set_text("%0.2d" % now.minute)

        self._set_combo_selection(self._dayCombo, now.day)
        self._set_combo_selection(self._monthCombo, now.month)
        self._set_combo_selection(self._yearCombo, now.year)

        #GLib's timer is driven by the return value of the function.
        #It runs the fuction periodically while the returned value
        #is True.
        return True

    def _save_system_time(self):
        """
        Returning False from this method removes the timer that would
        otherwise call it again and again.

        """

        self._start_updating_timer = None

        if not conf.system.can_set_system_clock:
            return False

        month = self._get_combo_selection(self._monthCombo)[0]
        if not month:
            return False

        year = self._get_combo_selection(self._yearCombo)[0]
        if not year:
            return False

        hours = int(self._hoursLabel.get_text())
        if not self._radioButton24h.get_active():
            hours = self._to_24h(hours, self._amPmLabel.get_text())

        minutes = int(self._minutesLabel.get_text())

        day = self._get_combo_selection(self._dayCombo)[0]
        #day may be None if there is no such in the selected year and month
        if day:
            isys.set_system_date_time(year,
                                      month,
                                      day,
                                      hours,
                                      minutes,
                                      tz=self._tz)

        #start the timer only when the spoke is shown
        if self._shown and not self._update_datetime_timer:
            self._update_datetime_timer = Timer()
            self._update_datetime_timer.timeout_sec(1, self._update_datetime)

        #run only once (after first 2 seconds of inactivity)
        return False

    def _stop_and_maybe_start_time_updating(self, interval=2):
        """
        This method is called in every date/time-setting button's callback.
        It removes the timer for updating displayed date/time (do not want to
        change it while user does it manually) and allows us to set new system
        date/time only after $interval seconds long idle on time-setting buttons.
        This is done by the _start_updating_timer that is reset in this method.
        So when there is $interval seconds long idle on date/time-setting
        buttons, self._save_system_time method is invoked. Since it returns
        False, this timer is then removed and only reactivated in this method
        (thus in some date/time-setting button's callback).

        """

        #do not start timers if the spoke is not shown
        if not self._shown:
            self._update_datetime()
            self._save_system_time()
            return

        #stop time updating
        if self._update_datetime_timer:
            self._update_datetime_timer.cancel()
            self._update_datetime_timer = None

        #stop previous $interval seconds timer (see below)
        if self._start_updating_timer:
            self._start_updating_timer.cancel()

        #let the user change date/time and after $interval seconds of inactivity
        #save it as the system time and start updating the displayed date/time
        self._start_updating_timer = Timer()
        self._start_updating_timer.timeout_sec(interval,
                                               self._save_system_time)

    def _set_combo_selection(self, combo, item):
        model = combo.get_model()
        if not model:
            return False

        itr = model.get_iter_first()
        while itr:
            if model[itr][0] == item:
                combo.set_active_iter(itr)
                return True

            itr = model.iter_next(itr)

        return False

    def _get_combo_selection(self, combo):
        """
        Get the selected item of the combobox.

        :return: selected item or None

        """

        model = combo.get_model()
        itr = combo.get_active_iter()
        if not itr or not model:
            return None, None

        return model[itr][0], model[itr][1]

    def _restore_old_city_region(self):
        """Restore stored "old" (or last valid) values."""
        # check if there are old values to go back to
        if self._old_region and self._old_city:
            self._set_timezone(self._old_region + "/" + self._old_city)

    def on_up_hours_clicked(self, *args):
        self._stop_and_maybe_start_time_updating()

        hours = int(self._hoursLabel.get_text())

        if self._radioButton24h.get_active():
            new_hours = (hours + 1) % 24
        else:
            amPm = self._amPmLabel.get_text()
            #let's not deal with magical AM/PM arithmetics
            new_hours = self._to_24h(hours, amPm)
            new_hours, new_amPm = self._to_amPm((new_hours + 1) % 24)
            self._amPmLabel.set_text(new_amPm)

        new_hours_str = "%0.2d" % new_hours
        self._hoursLabel.set_text(new_hours_str)

    def on_down_hours_clicked(self, *args):
        self._stop_and_maybe_start_time_updating()

        hours = int(self._hoursLabel.get_text())

        if self._radioButton24h.get_active():
            new_hours = (hours - 1) % 24
        else:
            amPm = self._amPmLabel.get_text()
            #let's not deal with magical AM/PM arithmetics
            new_hours = self._to_24h(hours, amPm)
            new_hours, new_amPm = self._to_amPm((new_hours - 1) % 24)
            self._amPmLabel.set_text(new_amPm)

        new_hours_str = "%0.2d" % new_hours
        self._hoursLabel.set_text(new_hours_str)

    def on_up_minutes_clicked(self, *args):
        self._stop_and_maybe_start_time_updating()

        minutes = int(self._minutesLabel.get_text())
        minutes_str = "%0.2d" % ((minutes + 1) % 60)
        self._minutesLabel.set_text(minutes_str)

    def on_down_minutes_clicked(self, *args):
        self._stop_and_maybe_start_time_updating()

        minutes = int(self._minutesLabel.get_text())
        minutes_str = "%0.2d" % ((minutes - 1) % 60)
        self._minutesLabel.set_text(minutes_str)

    def on_updown_ampm_clicked(self, *args):
        self._stop_and_maybe_start_time_updating()

        if self._amPmLabel.get_text() == _("AM"):
            self._amPmLabel.set_text(_("PM"))
        else:
            self._amPmLabel.set_text(_("AM"))

    def on_region_changed(self, combo, *args):
        """
        :see: on_city_changed

        """

        region = self._get_active_region()

        if not region or region == self._old_region:
            # region entry being edited or old_value chosen, no action needed
            # @see: on_city_changed
            return

        self._citiesFilter.refilter()

        # Set the city to the first one available in this newly selected region.
        zone = self._regions_zones[region]
        firstCity = sorted(list(zone))[0]

        self._set_combo_selection(self._cityCombo, firstCity)
        self._old_region = region
        self._old_city = firstCity

    def on_city_changed(self, combo, *args):
        """
        ComboBox emits ::changed signal not only when something is selected, but
        also when its entry's text is changed. We need to distinguish between
        those two cases ('London' typed in the entry => no action until ENTER is
        hit etc.; 'London' chosen in the expanded combobox => update timezone
        map and do all necessary actions). Fortunately when entry is being
        edited, self._get_active_city returns None.

        """

        timezone = None

        region = self._get_active_region()
        city = self._get_active_city()

        if not region or not city or (region == self._old_region
                                      and city == self._old_city):
            # entry being edited or no change, no actions needed
            return

        if city and region:
            timezone = region + "/" + city
        else:
            # both city and region are needed to form a valid timezone
            return

        if region == "Etc":
            # Etc timezones cannot be displayed on the map, so let's reset the
            # location and manually set a highlight with no location pin.
            self._tzmap.clear_location()
            if city in ("GMT", "UTC"):
                offset = 0.0
            # The tzdb data uses POSIX-style signs for the GMT zones, which is
            # the opposite of whatever everyone else expects. GMT+4 indicates a
            # zone four hours west of Greenwich; i.e., four hours before. Reverse
            # the sign to match the libtimezone map.
            else:
                # Take the part after "GMT"
                offset = -float(city[3:])

            self._tzmap.set_selected_offset(offset)
            time.tzset()
        else:
            # we don't want the timezone-changed signal to be emitted
            self._tzmap.set_timezone(timezone)
            time.tzset()

        # update "old" values
        self._old_city = city

    def on_entry_left(self, entry, *args):
        # user clicked somewhere else or hit TAB => finished editing
        entry.emit("activate")

    def on_city_region_key_released(self, entry, event, *args):
        if event.type == Gdk.EventType.KEY_RELEASE and \
                event.keyval == Gdk.KEY_Escape:
            # editing canceled
            self._restore_old_city_region()

    def on_completion_match_selected(self, combo, model, itr):
        item = None
        if model and itr:
            item = model[itr][0]
        if item:
            self._set_combo_selection(combo, item)

    def on_city_region_text_entry_activated(self, entry):
        combo = entry.get_parent()

        # It's gotta be up there somewhere, right? right???
        while not isinstance(combo, Gtk.ComboBox):
            combo = combo.get_parent()

        model = combo.get_model()
        entry_text = entry.get_text().lower()

        for row in model:
            if entry_text == row[0].lower():
                self._set_combo_selection(combo, row[0])
                return

        # non-matching value entered, reset to old values
        self._restore_old_city_region()

    def on_month_changed(self, *args):
        self._stop_and_maybe_start_time_updating(interval=5)
        self._daysFilter.refilter()

    def on_day_changed(self, *args):
        self._stop_and_maybe_start_time_updating(interval=5)

    def on_year_changed(self, *args):
        self._stop_and_maybe_start_time_updating(interval=5)
        self._daysFilter.refilter()

    def on_location_changed(self, tz_map, location):
        if not location:
            return

        timezone = location.get_property('zone')

        # Updating the timezone will update the region/city combo boxes to match.
        # The on_city_changed handler will attempt to convert the timezone back
        # to a location and set it in the map, which we don't want, since we
        # already have a location. That's why we're here.
        with blockedHandler(self._cityCombo, self.on_city_changed):
            if self._set_timezone(timezone):
                # timezone successfully set
                self._tz = get_timezone(timezone)
                self._update_datetime()

    def on_timeformat_changed(self, button24h, *args):
        hours = int(self._hoursLabel.get_text())
        amPm = self._amPmLabel.get_text()

        #connected to 24-hour radio button
        if button24h.get_active():
            self._set_amPm_part_sensitive(False)
            new_hours = self._to_24h(hours, amPm)
            self._amPmRevealer.set_reveal_child(False)
        else:
            self._set_amPm_part_sensitive(True)
            new_hours, new_amPm = self._to_amPm(hours)
            self._amPmLabel.set_text(new_amPm)
            self._amPmRevealer.set_reveal_child(True)

        self._hoursLabel.set_text("%0.2d" % new_hours)

    def _set_date_time_setting_sensitive(self, sensitive):
        #contains all date/time setting widgets
        footer_alignment = self.builder.get_object("footerAlignment")
        footer_alignment.set_sensitive(sensitive)

    def _show_no_network_warning(self):
        self.set_warning(_("You need to set up networking first if you "\
                           "want to use NTP"))

    def _show_no_ntp_server_warning(self):
        self.set_warning(_("You have no working NTP server configured"))

    def on_ntp_switched(self, switch, *args):
        if switch.get_active():
            #turned ON
            if not conf.system.can_set_time_synchronization:
                #cannot touch runtime system, not much to do here
                return

            if not self._network_module.proxy.Connected:
                self._show_no_network_warning()
                switch.set_active(False)
                return
            else:
                self.clear_info()

                working_server = self._config_dialog.working_server
                if working_server is None:
                    self._show_no_ntp_server_warning()
                else:
                    #we need a one-time sync here, because chronyd would not change
                    #the time as drastically as we need
                    ntp.one_time_sync_async(working_server)

            ret = util.start_service(NTP_SERVICE)
            self._set_date_time_setting_sensitive(False)

            #if starting chronyd failed and chronyd is not running,
            #set switch back to OFF
            if (ret != 0) and not util.service_running(NTP_SERVICE):
                switch.set_active(False)

        else:
            #turned OFF
            if not conf.system.can_set_time_synchronization:
                #cannot touch runtime system, nothing to do here
                return

            self._set_date_time_setting_sensitive(True)
            ret = util.stop_service(NTP_SERVICE)

            #if stopping chronyd failed and chronyd is running,
            #set switch back to ON
            if (ret != 0) and util.service_running(NTP_SERVICE):
                switch.set_active(True)

            self.clear_info()

    def on_ntp_config_clicked(self, *args):
        self._config_dialog.refresh()

        with self.main_window.enlightbox(self._config_dialog.window):
            response = self._config_dialog.run()

        if response == 1:
            pools, servers = self._config_dialog.pools_servers
            self._timezone_module.proxy.SetNTPServers(
                ntp.pools_servers_to_internal(pools, servers))

            if self._config_dialog.working_server is None:
                self._show_no_ntp_server_warning()
            else:
                self.clear_info()
Пример #20
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 Account")

    @classmethod
    def should_run(cls, environment, data):
        """Should the spoke run?"""
        if not is_module_available(USERS):
            return False

        return FirstbootSpokeMixIn.should_run(environment, data)

    def __init__(self, *args):
        NormalSpoke.__init__(self, *args)
        GUISpokeInputCheckHandler.__init__(self)
        self._users_module = USERS.get_proxy()

    def initialize(self):
        NormalSpoke.initialize(self)
        self.initialize_start()
        # get object references from the builders
        self._password_entry = self.builder.get_object("password_entry")
        self._password_confirmation_entry = self.builder.get_object("password_confirmation_entry")
        self._password_bar = self.builder.get_object("password_bar")
        self._password_label = self.builder.get_object("password_label")
        self._enable_root_radio = self.builder.get_object("enable_root_radio")
        self._disable_root_radio = self.builder.get_object("disable_root_radio")
        self._root_password_ssh_login_override = self.builder.get_object("root_password_ssh_login_override")
        self._revealer = self.builder.get_object("password_revealer")

        # Install the password checks:
        # - Has a password been specified?
        # - If a password has been specified and there is data in the confirm box, do they match?
        # - How strong is the password?
        # - Does the password contain non-ASCII characters?

        # Setup the password checker for password checking
        self._checker = input_checking.PasswordChecker(
                initial_password_content=self.password,
                initial_password_confirmation_content=self.password_confirmation,
                policy_name=PASSWORD_POLICY_ROOT
        )
        # configure the checker for password checking
        self.checker.secret_type = constants.SecretType.PASSWORD
        # remove any placeholder texts if either password or confirmation field changes content from initial state
        self.checker.password.changed_from_initial_state.connect(self.remove_placeholder_texts)
        self.checker.password_confirmation.changed_from_initial_state.connect(self.remove_placeholder_texts)
        # connect UI updates to check results
        self.checker.checks_done.connect(self._checks_done)

        # check that the password is not empty
        self._empty_check = input_checking.PasswordEmptyCheck()
        # check that the content of the password field & the conformation field are the same
        self._confirm_check = input_checking.PasswordConfirmationCheck()
        # check password validity, quality and strength
        self._validity_check = input_checking.PasswordValidityCheck()
        # connect UI updates to validity check results
        self._validity_check.result.password_score_changed.connect(self.set_password_score)
        self._validity_check.result.status_text_changed.connect(self.set_password_status)
        # check if the password contains non-ascii characters
        self._ascii_check = input_checking.PasswordASCIICheck()

        # register the individual checks with the checker in proper order
        # 1) is the password non-empty ?
        # 2) are both entered passwords the same ?
        # 3) is the password valid according to the current password checking policy ?
        # 4) is the password free of non-ASCII characters ?
        self.checker.add_check(self._empty_check)
        self.checker.add_check(self._confirm_check)
        self.checker.add_check(self._validity_check)
        self.checker.add_check(self._ascii_check)

        # Set placeholders if the password has been set outside of the Anaconda
        # GUI we either don't really know anything about it if it's crypted
        # and still would not really want to expose it if its set in plaintext,
        # and thus can'treally show it in the UI in any meaningful way.
        if self._users_module.IsRootPasswordSet:
            password_set_message = _("Root password has been set.")
            self.password_entry.set_placeholder_text(password_set_message)
            self.password_confirmation_entry.set_placeholder_text(password_set_message)

        # Configure levels for the password bar
        self._password_bar.add_offset_value("low", 2)
        self._password_bar.add_offset_value("medium", 3)
        self._password_bar.add_offset_value("high", 4)

        # set visibility of the password entries
        # - without this the password visibility toggle icon will
        #   not be shown
        set_password_visibility(self.password_entry, False)
        set_password_visibility(self.password_confirmation_entry, False)

        # Send ready signal to main event loop
        hubQ.send_ready(self.__class__.__name__)

        # report that we are done
        self.initialize_done()

    def refresh(self):
        # set the locked/unlocked state based on DBus data
        if self._users_module.IsRootAccountLocked:
            control = self._disable_root_radio
        else:
            control = self._enable_root_radio
        control.set_active(True)
        self.on_root_enabled_changed(control)

        self._root_password_ssh_login_override.set_active(
            self._users_module.RootPasswordSSHLoginAllowed
        )
        if self.root_enabled:
            # rerun checks so that we have a correct status message, if any
            self.checker.run_checks()
        # focus on the password field if it is sensitive
        if self.password_entry.get_sensitive():
            self.password_entry.grab_focus()

    @property
    def status(self):
        if self._users_module.IsRootAccountLocked:
            # reconfig mode currently allows re-enabling a locked root account if
            # user sets a new root password
            if is_reconfiguration_mode() and not self.root_enabled:
                return _("Disabled, set password to enable.")
            else:
                return _("Root account is disabled.")

        elif self._users_module.IsRootPasswordSet:
            return _("Root password is set")
        else:
            return _("Root password is not set")

    @property
    def mandatory(self):
        """Only mandatory if no admin user has been requested."""
        return not self._users_module.CheckAdminUserExists()

    def apply(self):
        pw = self.password

        enabled = self.root_enabled
        self._users_module.SetRootAccountLocked(not enabled)

        if enabled:
            # the checkbox makes it possible to override the default Open SSH
            # policy of not allowing root to login with password
            ssh_login_override = self._root_password_ssh_login_override.get_active()
            self._users_module.SetRootPasswordSSHLoginAllowed(ssh_login_override)

        if not pw:
            self._users_module.ClearRootPassword()
            return

        # we have a password - set it to kickstart data
        self._users_module.SetCryptedRootPassword(crypt_password(pw))

        # clear any placeholders
        self.remove_placeholder_texts()

        # Send ready signal to main event loop
        hubQ.send_ready(self.__class__.__name__)

    @property
    def completed(self):
        return self._users_module.IsRootPasswordSet

    @property
    def sensitive(self):
        # A password set in kickstart can be changed in the GUI
        # if the changesok password policy is set for the root password.
        kickstarted_password_can_be_changed = conf.ui.can_change_root or \
            self._users_module.CanChangeRootPassword

        return not (self.completed and flags.automatedInstall
                    and not kickstarted_password_can_be_changed)

    @property
    def root_enabled(self):
        return self._enable_root_radio.get_active()

    def _checks_done(self, error_message):
        """Update the warning with the input validation error from the first
           error message or clear warnings if all the checks were successful.

           Also appends the "press twice" suffix if compatible with current
           password policy and handles the press-done-twice logic.
        """
        # check if an unwaivable check failed
        unwaivable_check_failed = not self._confirm_check.result.success

        # set appropriate status bar message
        if not error_message or not self.root_enabled:
            # all is fine, just clear the message
            self.clear_info()
        elif not self.password and not self.password_confirmation:
            # Clear any info message if both the password and password
            # confirmation fields are empty.
            # This shortcut is done to make it possible for the user to leave the spoke
            # without inputting any root password. Separate logic makes sure an
            # empty string is not set as the root password.
            self.clear_info()
        else:
            if self.checker.policy.is_strict or unwaivable_check_failed:
                # just forward the error message
                self.show_warning_message(error_message)
            else:
                # add suffix for the click twice logic
                self.show_warning_message("{} {}".format(error_message,
                                                         _(constants.PASSWORD_DONE_TWICE)))

        # check if the spoke can be exited after the latest round of checks
        self._check_spoke_exit_conditions(unwaivable_check_failed)

    def _check_spoke_exit_conditions(self, unwaivable_check_failed):
        # Check if the user can escape from the root spoke or stay forever !

        # reset any waiving in progress
        self.waive_clicks = 0

        # Depending on the policy we allow users to waive the password strength
        # and non-ASCII checks. If the policy is set to strict, the password
        # needs to be strong, but can still contain non-ASCII characters.
        self.can_go_back = False
        self.needs_waiver = True

        # This shortcut is done to make it possible for the user to leave the spoke
        # without inputting any root password. Separate logic makes sure an
        # empty string is not set as the root password.
        if not self.password and not self.password_confirmation:
            self.can_go_back = True
            self.needs_waiver = False
        elif self.checker.success:
            # if all checks were successful we can always go back to the hub
            self.can_go_back = True
            self.needs_waiver = False
        elif unwaivable_check_failed:
            self.can_go_back = False
        else:
            if self.checker.policy.is_strict:
                if not self._validity_check.result.success:
                    # failing validity check in strict
                    # mode prevents us from going back
                    self.can_go_back = False
                elif not self._ascii_check.result.success:
                    # but the ASCII check can still be waived
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False
            else:
                if not self._validity_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                elif not self._ascii_check.result.success:
                    self.can_go_back = True
                    self.needs_waiver = True
                else:
                    self.can_go_back = True
                    self.needs_waiver = False

    def on_password_changed(self, editable, data=None):
        """Tell checker that the content of the password field changed."""
        self.checker.password.content = self.password

    def on_password_confirmation_changed(self, editable, data=None):
        """Tell checker that the content of the password confirmation field changed."""
        self.checker.password_confirmation.content = self.password_confirmation

    def on_password_icon_clicked(self, entry, icon_pos, event):
        """Called by Gtk callback when the icon of a password entry is clicked."""
        set_password_visibility(entry, not entry.get_visibility())

    def on_back_clicked(self, button):
        # disable root if no password is entered
        if not self.password and not self.password_confirmation and \
                not self._users_module.IsRootPasswordSet:
            control = self._disable_root_radio
            control.set_active(True)
            self.on_root_enabled_changed(control)

        # the GUI spoke input check handler handles the rest of the spoke exit logic for us
        if self.try_to_go_back() or not self.root_enabled:
            NormalSpoke.on_back_clicked(self, button)
        else:
            log.info("Return to hub prevented by password checking rules.")

    def on_root_enabled_changed(self, control):
        """Click event handler for root account enable and disable radio buttons."""
        unlocked = (control == self._enable_root_radio)
        self._revealer.set_reveal_child(unlocked)
        if unlocked:
            self.password_entry.grab_focus()
Пример #21
0
class LangsupportSpoke(LangLocaleHandler, NormalSpoke):
    """
       .. inheritance-diagram:: LangsupportSpoke
          :parts: 3
    """
    builderObjects = [
        "languageStore", "languageStoreFilter", "localeStore",
        "langsupportWindow"
    ]
    mainWidgetName = "langsupportWindow"
    focusWidgetName = "languageEntry"
    uiFile = "spokes/language_support.glade"
    helpFile = "LangSupportSpoke.xml"

    category = LocalizationCategory

    icon = "accessories-character-map-symbolic"
    title = CN_("GUI|Spoke", "_LANGUAGE SUPPORT")

    def __init__(self, *args, **kwargs):
        NormalSpoke.__init__(self, *args, **kwargs)
        LangLocaleHandler.__init__(self)
        self._selected_locales = set()

    def initialize(self):
        self.initialize_start()
        self._languageStore = self.builder.get_object("languageStore")
        self._languageEntry = self.builder.get_object("languageEntry")
        self._languageStoreFilter = self.builder.get_object(
            "languageStoreFilter")
        self._langView = self.builder.get_object("languageView")
        self._langSelectedRenderer = self.builder.get_object(
            "langSelectedRenderer")
        self._langSelectedColumn = self.builder.get_object(
            "langSelectedColumn")
        self._langSelection = self.builder.get_object("languageViewSelection")
        self._localeStore = self.builder.get_object("localeStore")
        self._localeView = self.builder.get_object("localeView")

        LangLocaleHandler.initialize(self)

        # mark selected locales and languages with selected locales bold
        localeNativeColumn = self.builder.get_object("localeNativeName")
        localeNativeNameRenderer = self.builder.get_object(
            "localeNativeNameRenderer")
        override_cell_property(localeNativeColumn, localeNativeNameRenderer,
                               "weight", self._mark_selected_locale_bold)

        languageNameColumn = self.builder.get_object("nameColumn")
        nativeNameRenderer = self.builder.get_object("nativeNameRenderer")
        englishNameRenderer = self.builder.get_object("englishNameRenderer")
        override_cell_property(languageNameColumn, nativeNameRenderer,
                               "weight", self._mark_selected_language_bold)
        override_cell_property(languageNameColumn, englishNameRenderer,
                               "weight", self._mark_selected_language_bold)

        # If a language has selected locales, highlight every column so that
        # the row appears highlighted
        for col in self._langView.get_columns():
            for rend in col.get_cells():
                override_cell_property(col, rend, "cell-background-rgba",
                                       self._highlight_selected_language)

        # and also set an icon so that we don't depend on a color to convey information
        highlightedColumn = self.builder.get_object("highlighted")
        highlightedRenderer = self.builder.get_object("highlightedRenderer")
        override_cell_property(highlightedColumn, highlightedRenderer,
                               "icon-name", self._render_lang_highlighted)

        # report that we are done
        self.initialize_done()

    def apply(self):
        # store only additional langsupport locales
        self.data.lang.addsupport = sorted(self._selected_locales -
                                           set([self.data.lang.lang]))

    def refresh(self):
        self._languageEntry.set_text("")
        self._selected_locales = set(self._installed_langsupports)

        # select the first locale from the "to be installed" langsupports
        self._select_locale(self._installed_langsupports[0])

    @property
    def _installed_langsupports(self):
        return [self.data.lang.lang] + sorted(self.data.lang.addsupport)

    @property
    def showable(self):
        # don't show the language support spoke on live media and in single language mode
        return not flags.livecdInstall and not flags.singlelang

    @property
    def status(self):
        return ", ".join(
            localization.get_native_name(locale)
            for locale in self._installed_langsupports)

    @property
    def mandatory(self):
        return False

    @property
    def completed(self):
        return True

    def _filter_languages(self, langs):
        return self.instclass.filterSupportedLangs(self.data, langs)

    def _add_language(self, store, native, english, lang):
        native_span = '<span lang="%s">%s</span>' % \
                (escape_markup(lang), escape_markup(native))
        store.append([native_span, english, lang])

    def _filter_locales(self, lang, locales):
        return self.instclass.filterSupportedLocales(self.data, lang, locales)

    def _add_locale(self, store, native, locale):
        native_span = '<span lang="%s">%s</span>' % \
                (escape_markup(re.sub(r'\..*', '', locale)),
                 escape_markup(native))

        # native, locale, selected, additional
        store.append([
            native_span, locale, locale in self._selected_locales,
            locale != self.data.lang.lang
        ])

    def _mark_selected_locale_bold(self,
                                   column,
                                   renderer,
                                   model,
                                   itr,
                                   user_data=None):
        if model[itr][2]:
            return Pango.Weight.BOLD.real
        else:
            return Pango.Weight.NORMAL.real

    def _is_lang_selected(self, lang):
        lang_locales = set(localization.get_language_locales(lang))
        return not lang_locales.isdisjoint(self._selected_locales)

    def _mark_selected_language_bold(self,
                                     column,
                                     renderer,
                                     model,
                                     itr,
                                     user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return Pango.Weight.BOLD.real
        else:
            return Pango.Weight.NORMAL.real

    def _highlight_selected_language(self,
                                     column,
                                     renderer,
                                     model,
                                     itr,
                                     user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return _HIGHLIGHT_COLOR
        else:
            return None

    def _render_lang_highlighted(self,
                                 column,
                                 renderer,
                                 model,
                                 itr,
                                 user_data=None):
        if self._is_lang_selected(model[itr][2]):
            return "emblem-ok-symbolic"
        else:
            return None

    # Signal handlers.
    def on_locale_toggled(self, renderer, path):
        itr = self._localeStore.get_iter(path)
        row = self._localeStore[itr]

        row[2] = not row[2]

        if row[2]:
            self._selected_locales.add(row[1])
        else:
            self._selected_locales.remove(row[1])