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()
示例#2
0
class SoftwareSelectionCacheTestCase(unittest.TestCase):
    """Test the cache for the Software Selection spoke."""
    def setUp(self):
        """Set up the test."""
        self.environment_data = CompsEnvironmentData()
        self.environment_data.id = "e1"
        self.environment_data.optional_groups = ["g1", "g2", "g3", "g4", "g5"]

        self.dnf_manager = Mock(spec=DNFManager)

        self.dnf_manager.resolve_environment.return_value = True
        self.dnf_manager.get_environment_data.return_value = self.environment_data

        self.dnf_manager.get_group_data.side_effect = self._get_group_data
        self.dnf_manager.resolve_group.return_value = True

        self.cache = SoftwareSelectionCache(self.dnf_manager)

    def _get_group_data(self, group):
        """Mock the get_group_data method of the DNF manager."""
        data = CompsGroupData()
        data.id = group
        return data

    def test_available_environments(self):
        """Test the available_environments property."""
        self.dnf_manager.environments = []
        assert self.cache.available_environments == []

        self.dnf_manager.environments = ["e1", "e2"]
        assert self.cache.available_environments == ["e1", "e2"]

    def test_is_environment_selected(self):
        """Test the is_environment_selected method."""
        assert self.cache.is_environment_selected("e1") is False

        self.cache.select_environment("e1")
        assert self.cache.is_environment_selected("e1") is True

        self.cache.select_environment("")
        assert self.cache.is_environment_selected("e1") is False

    def test_environment(self):
        """Test the environment property."""
        assert self.cache.environment == ""

        self.cache.select_environment("e1")
        assert self.cache.environment == "e1"

    def test_available_groups(self):
        """Test the available_groups property."""
        self.cache.select_environment("e1")
        assert self.cache.available_groups == ["g1", "g2", "g3", "g4", "g5"]

        self.cache.select_environment("")
        assert self.cache.available_groups == []

    def test_groups(self):
        """Test the groups property."""
        self.environment_data.default_groups = ["g2", "g4"]

        self.cache.select_environment("e1")
        assert self.cache.groups == ["g2", "g4"]

        self.cache.select_group("g1")
        assert self.cache.groups == ["g1", "g2", "g4"]

        self.cache.deselect_group("g4")
        assert self.cache.groups == ["g1", "g2"]

    def test_is_group_selected(self):
        """Test the is_group_selected method."""
        self.environment_data.default_groups = ["g2", "g4"]

        self.cache.select_environment("e1")
        assert self.cache.is_group_selected("g1") is False
        assert self.cache.is_group_selected("g4") is True
        assert self.cache.is_group_selected("g7") is False

        self.cache.select_group("g1")
        assert self.cache.is_group_selected("g1") is True
        assert self.cache.is_group_selected("g4") is True
        assert self.cache.is_group_selected("g7") is False

        self.cache.deselect_group("g4")
        assert self.cache.is_group_selected("g1") is True
        assert self.cache.is_group_selected("g4") is False
        assert self.cache.is_group_selected("g7") is False

    def test_apply_selection_data(self):
        """Test the apply_selection_data method."""
        selection = PackagesSelectionData()
        selection.environment = "e1"
        selection.groups = ["g1", "g2", "g3"]

        self.cache.apply_selection_data(selection)
        assert self.cache.environment == "e1"
        assert self.cache.groups == ["g1", "g2", "g3"]

    def test_apply_selection_data_default_environment(self):
        """Test the apply_selection_data method with a default environment."""
        self.dnf_manager.default_environment = "e1"
        self.dnf_manager.resolve_environment.return_value = False

        selection = PackagesSelectionData()
        selection.environment = "e2"

        self.cache.apply_selection_data(selection)
        assert self.cache.environment == "e1"
        assert self.cache.groups == []

    def test_apply_selection_data_invalid_environment(self):
        """Test the apply_selection_data method with an invalid environment."""
        self.dnf_manager.default_environment = ""
        self.dnf_manager.resolve_environment.return_value = False

        selection = PackagesSelectionData()
        selection.environment = "e2"

        self.cache.apply_selection_data(selection)
        assert self.cache.environment == ""
        assert self.cache.groups == []

    def test_apply_selection_data_invalid_groups(self):
        """Test the apply_selection_data method with invalid groups."""
        self.dnf_manager.resolve_group.return_value = False

        selection = PackagesSelectionData()
        selection.environment = "e1"
        selection.groups = ["g1", "g2", "g3"]

        self.cache.apply_selection_data(selection)
        assert self.cache.environment == "e1"
        assert self.cache.groups == []

    def test_get_selection_data(self):
        """Test the get_selection_data method."""
        self.cache.select_environment("e1")
        self.cache.select_group("g1")
        self.cache.select_group("g2")
        self.cache.select_group("g3")

        expected = PackagesSelectionData()
        expected.environment = "e1"
        expected.groups = ["g1", "g2", "g3"]

        data = self.cache.get_selection_data()
        assert compare_data(data, expected)

    def test_default_selection(self):
        """Test the default environment and group selection."""
        self.environment_data.id = "e1"
        self.environment_data.default_groups = ["g2", "g4"]

        self.cache.select_environment("e1")
        assert self.cache.groups == ["g2", "g4"]

        self.cache.select_group("g2")
        assert self.cache.groups == ["g2", "g4"]

        self.cache.select_group("g4")
        assert self.cache.groups == ["g2", "g4"]

        self.environment_data.id = "e2"
        self.environment_data.default_groups = ["g1", "g3", "g5"]

        self.cache.select_environment("e2")
        assert self.cache.groups == ["g1", "g3", "g5"]

    def test_selection(self):
        """Test the environment and group selection."""
        self.environment_data.id = "e1"

        self.cache.select_environment("e1")
        assert self.cache.groups == []

        self.cache.select_group("g2")
        assert self.cache.groups == ["g2"]

        self.cache.select_group("g4")
        assert self.cache.groups == ["g2", "g4"]

        self.environment_data.id = "e2"
        self.environment_data.default_groups = ["g1", "g3", "g5"]

        self.cache.select_environment("e2")
        assert self.cache.groups == ["g1", "g2", "g3", "g4", "g5"]

    def test_deselection(self):
        """Test the environment and group deselection."""
        self.environment_data.id = "e1"
        self.environment_data.default_groups = ["g2", "g4"]

        self.cache.select_environment("e1")
        assert self.cache.groups == ["g2", "g4"]

        self.cache.select_group("g1")
        assert self.cache.groups == ["g1", "g2", "g4"]

        self.cache.deselect_group("g4")
        assert self.cache.groups == ["g1", "g2"]

        self.cache.select_group("g5")
        assert self.cache.groups == ["g1", "g2", "g5"]

        self.cache.deselect_group("g5")
        assert self.cache.groups == ["g1", "g2"]

        self.environment_data.id = "e2"
        self.environment_data.default_groups = ["g2", "g3", "g4", "g5"]

        self.cache.select_environment("e2")
        assert self.cache.groups == ["g1", "g2", "g3"]
示例#3
0
class SoftwareSpoke(NormalTUISpoke):
    """The spoke for choosing the software.

       .. inheritance-diagram:: SoftwareSpoke
          :parts: 3
    """
    helpFile = "SoftwareSpoke.txt"
    category = SoftwareCategory

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

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

        return context.payload_type == PAYLOAD_TYPE_DNF

    def __init__(self, data, storage, payload):
        super().__init__(data, storage, payload)
        self.title = N_("Software selection")
        self._container = None
        self._tx_id = None
        self._errors = []
        self._warnings = []

        # Get the packages configuration.
        self._selection_cache = SoftwareSelectionCache(self._dnf_manager)

        # Are we taking values (package list) from a kickstart file?
        self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted

    @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()

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

    def _initialize_selection(self):
        """Initialize and check the software selection."""
        if not self._source_is_set:
            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 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 setup(self, args):
        """Set up the spoke right before it is used."""
        super().setup(args)

        # Wait for the payload to be ready.
        threadMgr.wait(THREAD_SOFTWARE_WATCHER)
        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)

        return True

    def refresh(self, args=None):
        """ Refresh screen. """
        NormalTUISpoke.refresh(self, args)
        self._container = None

        if not self._source_is_set:
            message = TextWidget(
                _("Installation source needs to be set up first."))
            self.window.add_with_separator(message)
            return

        threadMgr.wait(THREAD_CHECK_SOFTWARE)
        self._container = ListColumnContainer(columns=2,
                                              columns_width=38,
                                              spacing=2)

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

            widget = CheckboxWidget(title=data.name, completed=selected)

            self._container.add(widget,
                                callback=self._select_environment,
                                data=data.id)

        self.window.add_with_separator(TextWidget(_("Base environment")))
        self.window.add_with_separator(self._container)

        if self._errors or self._warnings:
            messages = "\n".join(self._errors or self._warnings)
            self.window.add_with_separator(TextWidget(messages))

    def _select_environment(self, data):
        self._selection_cache.select_environment(data)

    def input(self, args, key):
        """Handle the user input."""
        if self._container is None:
            return super().input(args, key)

        if self._container.process_user_input(key):
            return InputState.PROCESSED_AND_REDRAW

        if key.lower() == Prompt.CONTINUE:
            if self._selection_cache.environment:
                # The environment was selected, switch the screen.
                spoke = AdditionalSoftwareSpoke(self.data, self.storage,
                                                self.payload,
                                                self._selection_cache)
                ScreenHandler.push_screen_modal(spoke)
                self.apply()
                self.execute()

            return InputState.PROCESSED_AND_CLOSE

        return super().input(args, key)

    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)

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

    def _check_software_selection(self):
        """Check the software selection."""
        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()

        self._errors = list(report.error_messages)
        self._warnings = list(report.warning_messages)
        self._tx_id = self.payload.tx_id

        print("\n".join(report.get_messages()))

    def closed(self):
        """The spoke has been closed."""
        super().closed()

        # Run the setup method again on entry.
        self.screen_ready = False