Skip to content

Commit

Permalink
Merge pull request #5414 from rvykydal/port-kernel-features-support-t…
Browse files Browse the repository at this point in the history
…o-fedora

Port kernel features support to fedora
  • Loading branch information
rvykydal authored Jan 30, 2024
2 parents 74c438a + 731d652 commit c0d803e
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 65 deletions.
3 changes: 3 additions & 0 deletions data/anaconda.conf
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,9 @@ password_policies =
user (quality 1, length 6, empty)
luks (quality 1, length 6)

# Should kernel options be shown in the software selection spoke?
show_kernel_options = True

[License]
# A path to EULA (if any)
#
Expand Down
1 change: 1 addition & 0 deletions data/profile.d/rhel.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ swap_is_recommended = True

[User Interface]
custom_stylesheet = /usr/share/anaconda/pixmaps/redhat.css
show_kernel_options = True

[License]
eula = /usr/share/redhat-release/EULA
4 changes: 4 additions & 0 deletions pyanaconda/core/configuration/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,7 @@ def _validate_policy_attributes(attrs):

if "quality" not in attrs:
raise ValueError("The minimal quality is not specified.")

@property
def show_kernel_options(self):
return self._get_option("show_kernel_options", bool)
8 changes: 8 additions & 0 deletions pyanaconda/modules/payloads/payload/dnf/dnf.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,11 @@ def post_install_with_tasks(self):
dnf_manager=self.dnf_manager
)
]

def match_available_packages(self, pattern):
"""Find available packages that match the specified pattern.
:param pattern: a pattern for package names
:return: a list of matched package names
"""
return self.dnf_manager.match_available_packages(pattern)
8 changes: 8 additions & 0 deletions pyanaconda/modules/payloads/payload/dnf/dnf_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,11 @@ def GetRepoConfigurations(self) -> List[Structure]:
return RepoConfigurationData.to_structure_list(
self.implementation.get_repo_configurations()
)

def MatchAvailablePackages(self, pattern: Str) -> List[Str]:
"""Find available packages that match the specified pattern.
:param pattern: a pattern for package names
:return: a list of matched package names
"""
return self.implementation.match_available_packages(pattern)
222 changes: 162 additions & 60 deletions pyanaconda/ui/gui/spokes/software_selection.glade

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions pyanaconda/ui/gui/spokes/software_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@
from pyanaconda.ui.gui.spokes.lib.detailederror import DetailedErrorDialog
from pyanaconda.ui.gui.spokes.lib.software_selection import GroupListBoxRow, SeparatorRow, \
EnvironmentListBoxRow
from pyanaconda.ui.gui.utils import escape_markup
from pyanaconda.ui.lib.software import SoftwareSelectionCache, get_software_selection_status, \
is_software_selection_complete, get_group_data, get_environment_data
from pyanaconda.ui.lib.subscription import is_cdn_registration_required
from pyanaconda.ui.lib.software import FEATURE_64K, KernelFeatures, get_kernel_from_properties, \
get_available_kernel_features, get_kernel_titles_and_descriptions
from pyanaconda.core.configuration.anaconda import conf

import gi
gi.require_version("Gtk", "3.0")
Expand Down Expand Up @@ -94,6 +98,23 @@ def __init__(self, *args, **kwargs):
Gtk.Scrollable.get_vadjustment(addon_viewport)
)

# Display a group of options for selecting desired properties of a kernel
self._kernel_box = self.builder.get_object("kernelBox")
self._combo_kernel_page_size = self.builder.get_object("kernelPageSizeCombo")
self._label_kernel_page_size = self.builder.get_object("kernelPageSizeLabel")

# Normally I would create these in the .glade file but due to a bug they weren't
# created properly
self._model_kernel_page_size = Gtk.ListStore(str, str)

kernel_labels = get_kernel_titles_and_descriptions()
for i in ["4k", "64k"]:
self._model_kernel_page_size.append([i, "<b>%s</b>\n%s" %
(escape_markup(kernel_labels[i][0]),
escape_markup(kernel_labels[i][1]))])
self._combo_kernel_page_size.set_model(self._model_kernel_page_size)
self._available_kernels = get_available_kernel_features(self.payload.proxy)

@property
def _selection(self):
"""The packages selection."""
Expand Down Expand Up @@ -214,10 +235,12 @@ def refresh(self):
# Create a new software selection cache.
self._selection_cache = SoftwareSelectionCache(self.payload.proxy)
self._selection_cache.apply_selection_data(self._selection)
self._available_kernels = get_available_kernel_features(self.payload.proxy)

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

# Set up the info bar.
self.clear_info()
Expand Down Expand Up @@ -291,13 +314,50 @@ def _clear_listbox(self, listbox):
listbox.remove(child)
del child

def _refresh_kernel_features(self):
"""Display options for selecting kernel features."""

# Only showing parts of kernel box relevant for current system.
self._available_kernels = get_available_kernel_features(self.payload.proxy)

show_kernels = False
if conf.ui.show_kernel_options:
for (_key, val) in self._available_kernels.items():
if val:
show_kernels = True
break

if show_kernels:
self._kernel_box.set_visible(True)
self._kernel_box.set_no_show_all(False)

# Arm 64k page size kernel combo
self._combo_kernel_page_size.set_visible(self._available_kernels[FEATURE_64K])
self._combo_kernel_page_size.set_no_show_all(not self._available_kernels[FEATURE_64K])
self._label_kernel_page_size.set_visible(self._available_kernels[FEATURE_64K])
self._label_kernel_page_size.set_no_show_all(not self._available_kernels[FEATURE_64K])
else:
# Hide the entire box.
self._kernel_box.set_visible(False)
self._kernel_box.set_no_show_all(True)

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

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

# Select kernel
property_64k = self._available_kernels[FEATURE_64K] and \
self._combo_kernel_page_size.get_active_id() == FEATURE_64K
kernel_properties = KernelFeatures(property_64k)
kernel = get_kernel_from_properties(kernel_properties)
if kernel is not None and conf.ui.show_kernel_options:
log.debug("Selected kernel package: %s", kernel)
selection.packages.append(kernel)
selection.excluded_packages.append("kernel")

self.payload.set_packages_selection(selection)

hubQ.send_not_ready(self.__class__.__name__)
Expand Down
40 changes: 40 additions & 0 deletions pyanaconda/ui/lib/software.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
from collections import namedtuple
from blivet.arch import is_aarch64

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.i18n import _
from pyanaconda.modules.common.structures.comps import CompsEnvironmentData, CompsGroupData
from pyanaconda.modules.common.structures.packages import PackagesSelectionData

log = get_module_logger(__name__)

FEATURE_64K = "64k"
KernelFeatures = namedtuple("KernelFeatures", ["page_size_64k"])

def get_environment_data(dnf_proxy, environment_name):
"""Get the environment data.
Expand Down Expand Up @@ -96,6 +101,41 @@ def get_software_selection_status(dnf_proxy, selection, kickstarted=False):
return environment_data.name


def get_available_kernel_features(dnf_proxy):
"""Returns a dictionary with that shows which kernels should be shown in the UI.
"""
features = {
FEATURE_64K: is_aarch64() and any(dnf_proxy.MatchAvailablePackages("kernel-64k"))
}

return features


def get_kernel_titles_and_descriptions():
"""Returns a dictionary with descriptions and titles for different kernel options.
"""
kernel_features = {
"4k": (_("4k"), _("More efficient memory usage in smaller environments")),
"64k": (_("64k"), _("System performance gains for memory-intensive workloads")),
}

return kernel_features


def get_kernel_from_properties(features):
"""Translates the selection of required properties into a kernel package name and returns it
or returns None if no properties were selected.
"""
kernels = {
# ARM 64k Package Name
(False): None,
(True): "kernel-64k",
}

kernel_package = kernels[features[0]]
return kernel_package


class SoftwareSelectionCache(object):
"""The cache of the user software selection.
Expand Down
101 changes: 97 additions & 4 deletions pyanaconda/ui/tui/spokes/software_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
is_software_selection_complete, SoftwareSelectionCache, get_group_data, get_environment_data
from pyanaconda.ui.tui.spokes import NormalTUISpoke
from pyanaconda.core.threads import thread_manager
from pyanaconda.ui.lib.software import FEATURE_64K, KernelFeatures, \
get_kernel_from_properties, get_available_kernel_features, get_kernel_titles_and_descriptions
from pyanaconda.core.i18n import N_, _
from pyanaconda.core.constants import THREAD_PAYLOAD, THREAD_CHECK_SOFTWARE, \
THREAD_SOFTWARE_WATCHER, PAYLOAD_TYPE_DNF
from pyanaconda.core.configuration.anaconda import conf

from simpleline.render.containers import ListColumnContainer
from simpleline.render.prompt import Prompt
Expand Down Expand Up @@ -69,6 +72,8 @@ def __init__(self, data, storage, payload):

# Get the packages configuration.
self._selection_cache = SoftwareSelectionCache(self.payload.proxy)
self._kernel_selection = None
self._available_kernels = None

# Are we taking values (package list) from a kickstart file?
self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted
Expand All @@ -92,6 +97,9 @@ def _initialize(self):
"""Initialize the spoke in a separate thread."""
thread_manager.wait(THREAD_PAYLOAD)

self._available_kernels = get_available_kernel_features(self.payload.proxy)
self._kernel_selection = dict.fromkeys(self._available_kernels, False)

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

Expand Down Expand Up @@ -252,7 +260,8 @@ def input(self, args, key):
self.data,
self.storage,
self.payload,
self._selection_cache
self._selection_cache,
self._kernel_selection
)
ScreenHandler.push_screen_modal(spoke)
self.apply()
Expand All @@ -269,6 +278,19 @@ def apply(self):
selection = self._selection_cache.get_selection_data()
log.debug("Setting new software selection: %s", selection)

# Processing chosen kernel
if conf.ui.show_kernel_options:
self._available_kernels = get_available_kernel_features(self.payload.proxy)
feature_64k = self._available_kernels[FEATURE_64K] and \
self._kernel_selection[FEATURE_64K]
features = KernelFeatures(feature_64k)
kernel = get_kernel_from_properties(features)
if kernel:
log.debug("Selected kernel package: %s", kernel)
selection.packages.append(kernel)
selection.excluded_packages.append("kernel")

log.debug("Setting new software selection: %s", self._selection)
self.payload.set_packages_selection(selection)

def execute(self):
Expand Down Expand Up @@ -297,11 +319,12 @@ class AdditionalSoftwareSpoke(NormalTUISpoke):
"""The spoke for choosing the additional software."""
category = SoftwareCategory

def __init__(self, data, storage, payload, selection_cache):
def __init__(self, data, storage, payload, selection_cache, kernel_selection):
super().__init__(data, storage, payload)
self.title = N_("Software selection")
self._container = None
self._selection_cache = selection_cache
self._kernel_selection = kernel_selection

def refresh(self, args=None):
"""Refresh the screen."""
Expand Down Expand Up @@ -342,11 +365,81 @@ def _select_group(self, group):
else:
self._selection_cache.deselect_group(group)

def _show_kernel_features_screen(self, kernels):
"""Returns True if at least one non-standard kernel is available.
"""
if not conf.ui.show_kernel_options:
return False
for val in kernels.values():
if val:
return True
return False

def input(self, args, key):
if self._container.process_user_input(key):
return InputState.PROCESSED_AND_REDRAW
else:
return super().input(args, key)
if key.lower() == Prompt.CONTINUE:
available_kernels = get_available_kernel_features(self.payload.proxy)
if self._show_kernel_features_screen(available_kernels):
spoke = KernelSelectionSpoke(self.data, self.storage, self.payload,
self._selection_cache, self._kernel_selection,
available_kernels)
ScreenHandler.push_screen_modal(spoke)
self.execute()
self.close()
return InputState.PROCESSED

return super().input(args, key)

def apply(self):
pass


class KernelSelectionSpoke(NormalTUISpoke):
"""A subspoke for selecting kernel features.
"""
def __init__(self, data, storage, payload, selection_cache,
_kernel_selection, available_kernels):
super().__init__(data, storage, payload)
self.title = N_("Kernel Options")
self._container = None
self._selection_cache = selection_cache
self._kernel_selection = _kernel_selection
self._available_kernels = available_kernels

def refresh(self, args=None):
NormalTUISpoke.refresh(self)

# Retrieving translated UI strings
labels = get_kernel_titles_and_descriptions()

# Updating kernel availability
self._available_kernels = get_available_kernel_features(self.payload.proxy)
self._container = ListColumnContainer(2, columns_width=38, spacing=2)

# Rendering kernel checkboxes
for (name, val) in self._kernel_selection.items():
if not self._available_kernels[name]:
continue
(title, text) = labels[name]
widget = CheckboxWidget(title="%s" % title, text="%s" % text, completed=val)
self._container.add(widget, callback=self._set_kernel_callback, data=name)

self.window.add_with_separator(TextWidget(_("Kernel options")))
self.window.add_with_separator(self._container)

def _set_kernel_callback(self, data):
self._kernel_selection[data] = not self._kernel_selection[data]

def input(self, args, key):
if self._container.process_user_input(key):
return InputState.PROCESSED_AND_REDRAW

if key.lower() == Prompt.CONTINUE:
self.close()
return InputState.PROCESSED

return super().input(args, key)

def apply(self):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,16 @@ def test_url_get_repo_configurations(self, publisher):

assert self.interface.GetRepoConfigurations() == [expected]

def test_match_available_packages(self):
"""Test the MatchAvailablePackages method."""
assert self.interface.MatchAvailablePackages("p") == []

dnf_manager = Mock(spec=DNFManager)
dnf_manager.match_available_packages.return_value = ["p1", "p2"]
self.module._dnf_manager = dnf_manager

assert self.interface.MatchAvailablePackages("p") == ["p1", "p2"]


class DNFModuleTestCase(unittest.TestCase):
"""Test the DNF module."""
Expand Down
Loading

0 comments on commit c0d803e

Please sign in to comment.