488 lines
17 KiB
Python
488 lines
17 KiB
Python
|
#
|
||
|
# Kickstart module for DNF payload.
|
||
|
#
|
||
|
# Copyright (C) 2019 Red Hat, Inc.
|
||
|
#
|
||
|
# This copyrighted material is made available to anyone wishing to use,
|
||
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
||
|
# the GNU General Public License v.2, or (at your option) any later version.
|
||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||
|
# Public License for more details. You should have received a copy of the
|
||
|
# GNU General Public License along with this program; if not, write to the
|
||
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
||
|
# source code or documentation are not subject to the GNU General Public
|
||
|
# License and may only be used or replicated with the express permission of
|
||
|
# Red Hat, Inc.
|
||
|
#
|
||
|
from pyanaconda.anaconda_loggers import get_module_logger
|
||
|
from pyanaconda.core.configuration.anaconda import conf
|
||
|
from pyanaconda.core.signal import Signal
|
||
|
from pyanaconda.modules.common.structures.packages import PackagesConfigurationData, \
|
||
|
PackagesSelectionData
|
||
|
from pyanaconda.modules.payloads.constants import PayloadType, SourceType
|
||
|
from pyanaconda.modules.payloads.kickstart import convert_ks_repo_to_repo_data, \
|
||
|
convert_repo_data_to_ks_repo, convert_ks_data_to_packages_selection, \
|
||
|
convert_packages_selection_to_ksdata, convert_ks_data_to_packages_configuration, \
|
||
|
convert_packages_configuration_to_ksdata
|
||
|
from pyanaconda.modules.payloads.payload.dnf.dnf_manager import DNFManager
|
||
|
from pyanaconda.modules.payloads.payload.dnf.utils import protect_installation_devices, \
|
||
|
collect_installation_devices
|
||
|
from pyanaconda.modules.payloads.payload.dnf.validation import CheckPackagesSelectionTask, \
|
||
|
VerifyRepomdHashesTask
|
||
|
from pyanaconda.modules.payloads.payload.dnf.initialization import configure_dnf_logging, \
|
||
|
SetUpDNFSourcesTask, SetUpDNFSourcesResult, TearDownDNFSourcesTask
|
||
|
from pyanaconda.modules.payloads.payload.dnf.installation import SetRPMMacrosTask, \
|
||
|
ResolvePackagesTask, PrepareDownloadLocationTask, DownloadPackagesTask, InstallPackagesTask, \
|
||
|
CleanUpDownloadLocationTask, WriteRepositoriesTask, ImportRPMKeysTask, \
|
||
|
UpdateDNFConfigurationTask
|
||
|
from pyanaconda.modules.payloads.payload.dnf.tear_down import ResetDNFManagerTask
|
||
|
from pyanaconda.modules.payloads.payload.dnf.utils import calculate_required_space
|
||
|
from pyanaconda.modules.payloads.payload.payload_base import PayloadBase
|
||
|
from pyanaconda.modules.payloads.payload.dnf.dnf_interface import DNFInterface
|
||
|
from pyanaconda.modules.payloads.source.factory import SourceFactory
|
||
|
from pyanaconda.modules.payloads.source.utils import has_network_protocol
|
||
|
|
||
|
# Set up the modules logger.
|
||
|
log = get_module_logger(__name__)
|
||
|
|
||
|
# Configure the DNF logging.
|
||
|
configure_dnf_logging()
|
||
|
|
||
|
__all__ = ["DNFModule"]
|
||
|
|
||
|
|
||
|
class DNFModule(PayloadBase):
|
||
|
"""The DNF payload module."""
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Create a DNF module."""
|
||
|
super().__init__()
|
||
|
self._dnf_manager = DNFManager()
|
||
|
self._internal_sources = []
|
||
|
|
||
|
self._repositories = []
|
||
|
self.repositories_changed = Signal()
|
||
|
|
||
|
self._packages_configuration = PackagesConfigurationData()
|
||
|
self.packages_configuration_changed = Signal()
|
||
|
|
||
|
self._packages_selection = PackagesSelectionData()
|
||
|
self.packages_selection_changed = Signal()
|
||
|
|
||
|
self._packages_kickstarted = False
|
||
|
|
||
|
# Protect installation sources.
|
||
|
self._protected_devices = set()
|
||
|
self.sources_changed.connect(self._update_protected_devices)
|
||
|
self.repositories_changed.connect(self._update_protected_devices)
|
||
|
|
||
|
def for_publication(self):
|
||
|
"""Get the interface used to publish this source."""
|
||
|
return DNFInterface(self)
|
||
|
|
||
|
@property
|
||
|
def type(self):
|
||
|
"""Type of this payload."""
|
||
|
return PayloadType.DNF
|
||
|
|
||
|
@property
|
||
|
def default_source_type(self):
|
||
|
"""Type of the default source."""
|
||
|
return SourceType(conf.payload.default_source)
|
||
|
|
||
|
@property
|
||
|
def supported_source_types(self):
|
||
|
"""Get list of sources supported by DNF module."""
|
||
|
return [
|
||
|
SourceType.CDROM,
|
||
|
SourceType.HDD,
|
||
|
SourceType.HMC,
|
||
|
SourceType.NFS,
|
||
|
SourceType.REPO_FILES,
|
||
|
SourceType.REPO_PATH,
|
||
|
SourceType.CLOSEST_MIRROR,
|
||
|
SourceType.CDN,
|
||
|
SourceType.URL
|
||
|
]
|
||
|
|
||
|
def is_network_required(self):
|
||
|
"""Do the sources and repositories require a network?
|
||
|
|
||
|
:return: True or False
|
||
|
"""
|
||
|
# Check sources.
|
||
|
if super().is_network_required():
|
||
|
return True
|
||
|
|
||
|
# Check repositories.
|
||
|
for data in self.repositories:
|
||
|
if data.enabled and has_network_protocol(data.url):
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def dnf_manager(self):
|
||
|
"""The DNF manager of this payload."""
|
||
|
return self._dnf_manager
|
||
|
|
||
|
def _set_dnf_manager(self, dnf_manager):
|
||
|
"""Set the DNF manager of this payload."""
|
||
|
self._dnf_manager = dnf_manager
|
||
|
log.debug("The DNF manager is set.")
|
||
|
|
||
|
@property
|
||
|
def repositories(self):
|
||
|
"""The configuration of repositories.
|
||
|
|
||
|
:return [RepoConfigurationData]: a list of repo configurations
|
||
|
"""
|
||
|
return self._repositories
|
||
|
|
||
|
def set_repositories(self, data):
|
||
|
"""Set the configuration of repositories.
|
||
|
|
||
|
:param [RepoConfigurationData] data: a list of repo configurations
|
||
|
"""
|
||
|
self._repositories = data
|
||
|
self.repositories_changed.emit()
|
||
|
log.debug("Repositories are set to: %s", data)
|
||
|
|
||
|
@property
|
||
|
def packages_configuration(self):
|
||
|
"""The packages configuration.
|
||
|
|
||
|
:return: an instance of PackagesConfigurationData
|
||
|
"""
|
||
|
return self._packages_configuration
|
||
|
|
||
|
def set_packages_configuration(self, data):
|
||
|
"""Set the packages configuration.
|
||
|
|
||
|
:param data: an instance of PackagesConfigurationData
|
||
|
"""
|
||
|
self._packages_configuration = data
|
||
|
self.packages_configuration_changed.emit()
|
||
|
log.debug("Packages configuration is set to '%s'.", data)
|
||
|
|
||
|
@property
|
||
|
def packages_selection(self):
|
||
|
"""The packages selection.
|
||
|
|
||
|
:return: an instance of PackagesSelectionData
|
||
|
"""
|
||
|
return self._packages_selection
|
||
|
|
||
|
def set_packages_selection(self, data):
|
||
|
"""Set the packages selection.
|
||
|
|
||
|
:param data: an instance of PackagesSelectionData
|
||
|
"""
|
||
|
self._packages_selection = data
|
||
|
self.packages_selection_changed.emit()
|
||
|
log.debug("Packages selection is set to '%s'.", data)
|
||
|
|
||
|
@property
|
||
|
def packages_kickstarted(self):
|
||
|
"""Are the packages set from a kickstart?
|
||
|
|
||
|
FIXME: This is a temporary property.
|
||
|
|
||
|
:return: True or False
|
||
|
"""
|
||
|
return self._packages_kickstarted
|
||
|
|
||
|
def set_packages_kickstarted(self, value):
|
||
|
"""Are the packages set from a kickstart?"""
|
||
|
self._packages_kickstarted = value
|
||
|
log.debug("Are the packages set from a kickstart? %s", value)
|
||
|
|
||
|
def process_kickstart(self, data):
|
||
|
"""Process the kickstart data."""
|
||
|
self._process_kickstart_sources(data)
|
||
|
self._process_kickstart_repositories(data)
|
||
|
self._process_kickstart_packages(data)
|
||
|
|
||
|
def _process_kickstart_sources(self, data):
|
||
|
"""Process the kickstart sources."""
|
||
|
source_type = SourceFactory.get_rpm_type_for_kickstart(data)
|
||
|
|
||
|
if source_type is None:
|
||
|
return
|
||
|
|
||
|
source = SourceFactory.create_source(source_type)
|
||
|
source.process_kickstart(data)
|
||
|
self.add_source(source)
|
||
|
|
||
|
def _process_kickstart_repositories(self, data):
|
||
|
"""Process the kickstart repositories."""
|
||
|
repositories = list(map(
|
||
|
convert_ks_repo_to_repo_data,
|
||
|
data.repo.dataList()
|
||
|
))
|
||
|
self.set_repositories(repositories)
|
||
|
|
||
|
def _process_kickstart_packages(self, data):
|
||
|
"""Process the kickstart packages."""
|
||
|
selection = convert_ks_data_to_packages_selection(data)
|
||
|
self.set_packages_selection(selection)
|
||
|
self.set_packages_kickstarted(data.packages.seen)
|
||
|
|
||
|
configuration = convert_ks_data_to_packages_configuration(data)
|
||
|
self.set_packages_configuration(configuration)
|
||
|
|
||
|
def setup_kickstart(self, data):
|
||
|
"""Setup the kickstart data."""
|
||
|
self._set_up_kickstart_sources(data)
|
||
|
self._set_up_kickstart_repositories(data)
|
||
|
self._set_up_kickstart_packages(data)
|
||
|
|
||
|
def _set_up_kickstart_sources(self, data):
|
||
|
"""Set up the kickstart sources."""
|
||
|
for source in self.sources:
|
||
|
source.setup_kickstart(data)
|
||
|
|
||
|
def _set_up_kickstart_repositories(self, data):
|
||
|
"""Set up the kickstart repositories."""
|
||
|
# Don't include disabled repositories.
|
||
|
enabled_repositories = list(filter(
|
||
|
lambda r: r.enabled, self.repositories
|
||
|
))
|
||
|
data.repo.repoList = list(map(
|
||
|
convert_repo_data_to_ks_repo,
|
||
|
enabled_repositories
|
||
|
))
|
||
|
|
||
|
def _set_up_kickstart_packages(self, data):
|
||
|
"""Set up the kickstart packages selection."""
|
||
|
convert_packages_selection_to_ksdata(self.packages_selection, data)
|
||
|
convert_packages_configuration_to_ksdata(self.packages_configuration, data)
|
||
|
|
||
|
def _update_protected_devices(self):
|
||
|
"""Protect devices specified by installation sources."""
|
||
|
previous_devices = self._protected_devices
|
||
|
current_devices = collect_installation_devices(
|
||
|
self.sources,
|
||
|
self.repositories,
|
||
|
)
|
||
|
protect_installation_devices(
|
||
|
previous_devices,
|
||
|
current_devices,
|
||
|
)
|
||
|
self._protected_devices = current_devices
|
||
|
|
||
|
def get_available_repositories(self):
|
||
|
"""Get a list of available repositories.
|
||
|
|
||
|
:return: a list with names of available repositories
|
||
|
"""
|
||
|
return self.dnf_manager.repositories
|
||
|
|
||
|
def get_enabled_repositories(self):
|
||
|
"""Get a list of enabled repositories.
|
||
|
|
||
|
:return: a list with names of enabled repositories
|
||
|
"""
|
||
|
return self.dnf_manager.enabled_repositories
|
||
|
|
||
|
def get_default_environment(self):
|
||
|
"""Get a default environment.
|
||
|
|
||
|
:return: an identifier of an environment or an empty string
|
||
|
"""
|
||
|
return self.dnf_manager.default_environment or ""
|
||
|
|
||
|
def get_environments(self):
|
||
|
"""Get a list of environments defined in comps.xml files.
|
||
|
|
||
|
:return: a list with identifiers of environments
|
||
|
"""
|
||
|
return self.dnf_manager.environments
|
||
|
|
||
|
def resolve_environment(self, environment_spec):
|
||
|
"""Translate the given specification to an environment identifier.
|
||
|
|
||
|
:param environment_spec: an environment specification
|
||
|
:return: an identifier of an environment or an empty string
|
||
|
"""
|
||
|
return self.dnf_manager.resolve_environment(environment_spec) or ""
|
||
|
|
||
|
def get_environment_data(self, environment_spec):
|
||
|
"""Get data about the specified environment.
|
||
|
|
||
|
:param environment_spec: an environment specification
|
||
|
:return CompsEnvironmentData: the related environment data
|
||
|
:raise UnknownCompsEnvironmentError: if the environment is unknown
|
||
|
"""
|
||
|
return self.dnf_manager.get_environment_data(environment_spec)
|
||
|
|
||
|
def resolve_group(self, group_spec):
|
||
|
"""Translate the given specification into a group identifier.
|
||
|
|
||
|
:param group_spec: a group specification
|
||
|
:return: an identifier of a group or an empty string
|
||
|
"""
|
||
|
return self.dnf_manager.resolve_group(group_spec) or ""
|
||
|
|
||
|
def get_group_data(self, group_spec):
|
||
|
"""Get data about the specified group.
|
||
|
|
||
|
:param group_spec: a specification of a group
|
||
|
:return CompsGroupData: the related group data
|
||
|
:raise: UnknownCompsGroupError if the group is unknown
|
||
|
"""
|
||
|
return self.dnf_manager.get_group_data(group_spec)
|
||
|
|
||
|
def verify_repomd_hashes_with_task(self):
|
||
|
"""Verify a hash of the repomd.xml file for each enabled repository with a task.
|
||
|
|
||
|
This method tests if URL links from active repositories can be reached.
|
||
|
It is useful when network settings are changed so that we can verify if
|
||
|
repositories are still reachable. The task returns a validation report.
|
||
|
|
||
|
:return: a task
|
||
|
"""
|
||
|
return VerifyRepomdHashesTask(self.dnf_manager)
|
||
|
|
||
|
def validate_packages_selection_with_task(self, data):
|
||
|
"""Validate the specified packages selection.
|
||
|
|
||
|
Return a task for validation of the software selection.
|
||
|
The result of the task is a validation report.
|
||
|
|
||
|
:param PackagesSelectionData data: a packages selection
|
||
|
:return: a task
|
||
|
"""
|
||
|
return CheckPackagesSelectionTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
selection=data,
|
||
|
)
|
||
|
|
||
|
def calculate_required_space(self):
|
||
|
"""Calculate space required for the installation.
|
||
|
|
||
|
:return: required size in bytes
|
||
|
:rtype: int
|
||
|
"""
|
||
|
required_space = calculate_required_space(self.dnf_manager)
|
||
|
return required_space.get_bytes()
|
||
|
|
||
|
def get_repo_configurations(self):
|
||
|
"""Get RepoConfiguration structures for all sources.
|
||
|
|
||
|
These structures will be used by DNF payload in the main process.
|
||
|
|
||
|
FIXME: This is a temporary solution. Will be removed after DNF payload logic is moved.
|
||
|
|
||
|
:return: RepoConfiguration structures for attached sources.
|
||
|
:rtype: RepoConfigurationData instances
|
||
|
"""
|
||
|
return list(filter(None, [s.generate_repo_configuration() for s in self.sources]))
|
||
|
|
||
|
def set_up_sources_with_task(self):
|
||
|
"""Set up installation sources."""
|
||
|
task = SetUpDNFSourcesTask(
|
||
|
sources=self.sources,
|
||
|
repositories=self.repositories,
|
||
|
configuration=self.packages_configuration,
|
||
|
)
|
||
|
task.succeeded_signal.connect(
|
||
|
lambda: self._set_up_sources_on_success(task.get_result())
|
||
|
)
|
||
|
return task
|
||
|
|
||
|
def _set_up_sources_on_success(self, result: SetUpDNFSourcesResult):
|
||
|
"""Update the module based on the configured sources."""
|
||
|
self._set_dnf_manager(result.dnf_manager)
|
||
|
self.set_repositories(result.repositories)
|
||
|
self._internal_sources += result.sources
|
||
|
|
||
|
def tear_down_sources_with_task(self):
|
||
|
"""Tear down installation sources."""
|
||
|
return TearDownDNFSourcesTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
sources=self._internal_sources + self.sources
|
||
|
)
|
||
|
|
||
|
def install_with_tasks(self):
|
||
|
"""Install the payload.
|
||
|
|
||
|
:return: list of tasks
|
||
|
"""
|
||
|
tasks = [
|
||
|
SetRPMMacrosTask(
|
||
|
configuration=self.packages_configuration
|
||
|
),
|
||
|
ResolvePackagesTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
selection=self.packages_selection,
|
||
|
),
|
||
|
PrepareDownloadLocationTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
),
|
||
|
DownloadPackagesTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
),
|
||
|
InstallPackagesTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
),
|
||
|
CleanUpDownloadLocationTask(
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
),
|
||
|
]
|
||
|
|
||
|
self._collect_kernels_on_success(InstallPackagesTask, tasks)
|
||
|
return tasks
|
||
|
|
||
|
def _collect_kernels_on_success(self, task_class, tasks):
|
||
|
"""Collect kernel version lists from a task specified by its class.
|
||
|
|
||
|
Find an instance of the specified task class that should return
|
||
|
a kernel version list if successful. Connect to its signal and
|
||
|
update the kernel version list of the module when the task is
|
||
|
finished.
|
||
|
|
||
|
:param task_class: a class of a scheduled task
|
||
|
:param tasks: a list of scheduled tasks
|
||
|
"""
|
||
|
for task in tasks:
|
||
|
if isinstance(task, task_class):
|
||
|
task.succeeded_signal.connect(
|
||
|
lambda t=task: self.set_kernel_version_list(t.get_result())
|
||
|
)
|
||
|
|
||
|
def post_install_with_tasks(self):
|
||
|
"""Execute post installation steps.
|
||
|
|
||
|
:return: list of tasks
|
||
|
"""
|
||
|
return [
|
||
|
WriteRepositoriesTask(
|
||
|
sysroot=conf.target.system_root,
|
||
|
dnf_manager=self.dnf_manager,
|
||
|
repositories=self.repositories,
|
||
|
),
|
||
|
ImportRPMKeysTask(
|
||
|
sysroot=conf.target.system_root,
|
||
|
gpg_keys=conf.payload.default_rpm_gpg_keys
|
||
|
),
|
||
|
UpdateDNFConfigurationTask(
|
||
|
sysroot=conf.target.system_root,
|
||
|
configuration=self.packages_configuration,
|
||
|
),
|
||
|
ResetDNFManagerTask(
|
||
|
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)
|