anaconda/anaconda-40.22.3.13/pyanaconda/modules/payloads/payload/dnf/installation.py
2024-11-14 21:39:56 -08:00

422 lines
14 KiB
Python

#
# Copyright (C) 2020 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.
#
import os
import shutil
import rpm
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core import util
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.constants import RPM_LANGUAGES_NONE, RPM_LANGUAGES_ALL, MULTILIB_POLICY_BEST
from pyanaconda.core.i18n import _
from pyanaconda.core.path import join_paths, make_directories
from pyanaconda.modules.common.errors.installation import PayloadInstallationError, \
NonCriticalInstallationError
from pyanaconda.modules.common.structures.packages import PackagesConfigurationData
from pyanaconda.modules.common.task import Task
from pyanaconda.modules.payloads.payload.dnf.requirements import collect_remote_requirements, \
collect_language_requirements, collect_platform_requirements, \
collect_driver_disk_requirements, apply_requirements
from pyanaconda.modules.payloads.payload.dnf.utils import pick_download_location, \
get_kernel_version_list
from pyanaconda.modules.payloads.payload.dnf.validation import CheckPackagesSelectionTask
log = get_module_logger(__name__)
class SetRPMMacrosTask(Task):
"""Installation task to set RPM macros."""
def __init__(self, configuration: PackagesConfigurationData):
"""Create a task.
:param configuration: a packages configuration data
"""
super().__init__()
self._data = configuration
self._macros = []
@property
def name(self):
"""The name of the task."""
return "Set RPM macros"
def run(self):
"""Run the task."""
self._macros = self._collect_macros(self._data)
self._install_macros(self._macros)
def _collect_macros(self, data: PackagesConfigurationData):
"""Collect the RPM macros."""
macros = list()
# nofsync speeds things up at the risk of rpmdb data loss in a crash.
# But if we crash mid-install you're boned anyway, so who cares?
macros.append(('__dbi_htconfig', 'hash nofsync %{__dbi_other} %{__dbi_perms}'))
if data.docs_excluded:
macros.append(('_excludedocs', '1'))
if data.languages == RPM_LANGUAGES_NONE:
macros.append(('_install_langs', '%{nil}'))
elif data.languages != RPM_LANGUAGES_ALL:
macros.append(('_install_langs', data.languages))
if conf.security.selinux:
for d in ["/etc/selinux/targeted/contexts/files",
"/etc/security/selinux/src/policy",
"/etc/security/selinux"]:
f = d + "/file_contexts"
if os.access(f, os.R_OK):
macros.append(('__file_context_path', f))
break
else:
macros.append(('__file_context_path', '%{nil}'))
return macros
def _install_macros(self, macros):
"""Add RPM macros to the global transaction environment."""
for name, value in macros:
log.debug("Set '%s' to '%s'.", name, value)
rpm.addMacro(name, value) # pylint: disable=no-member
class ResolvePackagesTask(CheckPackagesSelectionTask):
"""Installation task to resolve the software selection."""
@property
def name(self):
"""The name of the task."""
return "Resolve packages"
def run(self):
"""Run the task.
:raise PayloadInstallationError: if the selection cannot be resolved
:raise NonCriticalInstallationError: if the selection is resolved with warnings
"""
report = super().run()
if report.error_messages:
message = "\n\n".join(report.error_messages)
log.error("The packages couldn't be resolved:\n\n%s", message)
raise PayloadInstallationError(message)
if report.warning_messages:
message = "\n\n".join(report.warning_messages)
log.warning("The packages were resolved with warnings:\n\n%s", message)
raise NonCriticalInstallationError(message)
@property
def _requirements(self):
"""Requirements for installing packages and groups.
:return: a list of requirements
"""
return collect_remote_requirements() \
+ collect_language_requirements(self._dnf_manager) \
+ collect_platform_requirements(self._dnf_manager) \
+ collect_driver_disk_requirements()
def _collect_required_specs(self):
"""Collect specs for the required software."""
super()._collect_required_specs()
# Apply requirements.
apply_requirements(self._requirements, self._include_list, self._exclude_list)
class PrepareDownloadLocationTask(Task):
"""The installation task for setting up the download location."""
def __init__(self, dnf_manager):
"""Create a new task.
:param dnf_manager: a DNF manager
"""
super().__init__()
self._dnf_manager = dnf_manager
@property
def name(self):
return "Prepare the package download"
def run(self):
"""Run the task.
:return: a path of the download location
"""
path = pick_download_location(self._dnf_manager)
if os.path.exists(path):
log.info("Removing existing package download location: %s", path)
shutil.rmtree(path)
self._dnf_manager.set_download_location(path)
return path
class CleanUpDownloadLocationTask(Task):
"""The installation task for cleaning up the download location."""
def __init__(self, dnf_manager):
"""Create a new task.
:param dnf_manager: a DNF manager
"""
super().__init__()
self._dnf_manager = dnf_manager
@property
def name(self):
return "Remove downloaded packages"
def run(self):
"""Run the task.
Some installation sources, such as NFS, don't need to download packages to
local storage, so the download location might not always exist. See the bug
1193121 for more information.
"""
path = self._dnf_manager.download_location
if not os.path.exists(path):
log.warning("The download location %s doesn't exist.", path)
return
log.info("Removing downloaded packages from %s.", path)
shutil.rmtree(path)
class DownloadPackagesTask(Task):
"""The installation task for downloading the packages."""
def __init__(self, dnf_manager):
"""Create a new task.
:param dnf_manager: a DNF manager
"""
super().__init__()
self._dnf_manager = dnf_manager
@property
def name(self):
return "Download packages"
def run(self):
self.report_progress(_("Downloading packages"))
self._dnf_manager.download_packages(self.report_progress)
class InstallPackagesTask(Task):
"""The installation task for installing the packages."""
def __init__(self, dnf_manager):
"""Create a new task.
:param dnf_manager: a DNF manager
"""
super().__init__()
self._dnf_manager = dnf_manager
@property
def name(self):
return "Install packages"
def run(self):
"""Run the task.
:return: a list of installed kernel versions
"""
self.report_progress(_("Preparing transaction from installation source"))
self._dnf_manager.install_packages(self.report_progress)
return get_kernel_version_list()
class WriteRepositoriesTask(Task):
"""The installation task for writing repositories on the target system."""
def __init__(self, sysroot, dnf_manager, repositories):
"""Create a new task.
:param str sysroot: a path to the system root
:param DNFManager dnf_manager: a DNF manager
:param [RepoConfigurationData] repositories: a list of repo data
"""
super().__init__()
self._sysroot = sysroot
self._dnf_manager = dnf_manager
self._repositories = repositories
@property
def name(self):
return "Write repositories"
def run(self):
"""Run the task."""
for repo in self._repositories:
if not self._can_write_repo(repo):
log.debug("Couldn't write %s.repo to the target system.", repo.name)
continue
log.info("Writing %s.repo to the target system.", repo.name)
content = self._dnf_manager.generate_repo_file(repo)
self._write_repo_file(repo.name, content)
def _can_write_repo(self, repo):
"""Can we write the specified repository to the target system?
* Skip repositories that are not allowed to be installed.
* Skip repositories from the installation environment.
* Support only http, https and ftp protocols.
"""
supported_protocols = [
"http:",
"https:",
"ftp:"
]
if not repo.installation_enabled:
log.debug("Installation of the repository is not allowed.")
return False
if not repo.name:
log.debug("The name of the repository is not specified.")
return False
if not repo.url:
log.debug("The URL of the repository is not specified.")
return False
if not any(repo.url.startswith(p) for p in supported_protocols):
log.debug("The repository uses an unsupported protocol.")
return False
return True
def _write_repo_file(self, repo_name, content):
"""Write the specified content into a repo file."""
repo_dir = join_paths(
self._sysroot,
"etc/yum.repos.d/"
)
make_directories(repo_dir)
repo_path = join_paths(
repo_dir,
repo_name + ".repo"
)
with open(repo_path, "w") as f:
f.write(content.strip() + "\n")
class ImportRPMKeysTask(Task):
"""The installation task for import of the RPM keys."""
def __init__(self, sysroot, gpg_keys):
"""Create a new task.
:param sysroot: a path to the system root
:param gpg_keys: a list of gpg keys to import
"""
super().__init__()
self._sysroot = sysroot
self._gpg_keys = gpg_keys
@property
def name(self):
return "Import RPM keys"
def run(self):
"""Run the task"""
if not self._gpg_keys:
log.debug("No GPG keys to import.")
return
if not os.path.exists(self._sysroot + "/usr/bin/rpm"):
log.error(
"Can not import GPG keys to RPM database because "
"the 'rpm' executable is missing on the target "
"system. The following keys were not imported:\n%s",
"\n".join(self._gpg_keys)
)
return
# Get substitutions for variables.
# TODO: replace the interpolation with DNF once possible
basearch = os.uname().machine
releasever = util.get_os_release_value("VERSION_ID", sysroot=self._sysroot) or ""
# Import GPG keys to RPM database.
for key in self._gpg_keys:
key = key.replace("$releasever", releasever).replace("$basearch", basearch)
log.info("Importing GPG key to RPM database: %s", key)
rc = util.execWithRedirect("rpm", ["--import", key], root=self._sysroot)
if rc:
log.error("Failed to import the GPG key.")
class UpdateDNFConfigurationTask(Task):
"""The installation task to update the dnf.conf file."""
def __init__(self, sysroot, configuration: PackagesConfigurationData):
"""Create a new task.
:param sysroot: a path to the system root
:param configuration: a packages configuration data
"""
super().__init__()
self._sysroot = sysroot
self._data = configuration
@property
def name(self):
return "Update DNF configuration"
def run(self):
"""Run the task."""
if self._data.multilib_policy != MULTILIB_POLICY_BEST:
self._set_option("multilib_policy", self._data.multilib_policy)
def _set_option(self, option, value):
"""Set a configuration option.
:param option: a name of the option
:param value: a value of the option
"""
log.debug("Setting '%s' to '%s'.", option, value)
cmd = "dnf"
args = [
"config-manager",
"--save",
"--setopt={}={}".format(option, value),
]
try:
rc = util.execWithRedirect(cmd, args, root=self._sysroot)
except OSError as e:
log.warning("Couldn't update the DNF configuration: %s", e)
return
if rc != 0:
log.warning("Failed to update the DNF configuration (%s).", rc)
return