# # 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