anaconda/anaconda-40.22.3.13/pyanaconda/modules/payloads/payload/dnf/utils.py

429 lines
14 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
#
# 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 fnmatch
import hashlib
import os
import rpm
from libdnf.transaction import TransactionItemState_ERROR
from blivet.size import Size
from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.payload import parse_hdd_url
from pyanaconda.core.regexes import VERSION_DIGITS
from pyanaconda.core.util import execWithCapture
from pyanaconda.core.hw import is_lpae_available
from pyanaconda.core.path import join_paths
from pyanaconda.modules.common.constants.objects import DEVICE_TREE, DISK_SELECTION
from pyanaconda.modules.common.constants.services import STORAGE
from pyanaconda.modules.common.structures.packages import PackagesSelectionData
from pyanaconda.modules.payloads.constants import SourceType
from pyanaconda.core.product import get_product_name, get_product_version
from pyanaconda.modules.payloads.base.utils import sort_kernel_version_list
log = get_module_logger(__name__)
DNF_PACKAGE_CACHE_DIR_SUFFIX = 'dnf.package.cache'
def calculate_hash(data):
"""Calculate hash from the given data.
:return: a string with the hash
"""
m = hashlib.sha256()
m.update(data.encode('ascii', 'backslashreplace'))
return m.digest()
def get_kernel_package(dnf_manager, exclude_list):
"""Get an installable kernel package.
:param dnf_manager: a DNF manager
:param exclude_list: a list of excluded packages
:return: a package name or None
"""
if "kernel" in exclude_list:
return None
# Get the kernel packages.
kernels = ["kernel"]
# ARM systems use either the standard Multiplatform or LPAE platform.
if is_lpae_available():
kernels.insert(0, "kernel-lpae")
# Find an installable one.
for kernel_package in kernels:
if kernel_package in exclude_list:
continue
if not dnf_manager.is_package_available(kernel_package):
log.info("No such package: %s", kernel_package)
continue
return kernel_package
log.error("Failed to select a kernel from: %s", kernels)
return None
def get_product_release_version():
"""Get a release version of the product.
:return: a string with the release version
"""
try:
release_version = VERSION_DIGITS.match(get_product_version()).group(1)
except AttributeError:
release_version = "rawhide"
log.debug("Release version of %s is %s.", get_product_name(), release_version)
return release_version
def get_installation_specs(data: PackagesSelectionData, default_environment=None):
"""Get specifications of packages, groups and modules for installation.
:param data: a packages selection data
:param default_environment: a default environment to install
:return: a tuple of specification lists for inclusion and exclusion
"""
# Note about package/group/module spec formatting:
# - leading @ signifies a group or module
# - no leading @ means a package
include_list = []
exclude_list = []
# Handle the environment.
if data.default_environment_enabled and default_environment:
log.info("Selecting default environment '%s'.", default_environment)
include_list.append("@{}".format(default_environment))
elif data.environment:
include_list.append("@{}".format(data.environment))
# Handle the core group.
if not data.core_group_enabled:
log.info("Skipping @core group; system may not be complete.")
exclude_list.append("@core")
else:
include_list.append("@core")
# Handle groups.
for group_name in data.excluded_groups:
exclude_list.append("@{}".format(group_name))
for group_name in data.groups:
# Packages in groups can have different types
# and we provide an option to users to set
# which types are going to be installed.
if group_name in data.groups_package_types:
type_list = data.groups_package_types[group_name]
group_spec = "@{group_name}/{types}".format(
group_name=group_name,
types=",".join(type_list)
)
else:
# If group is a regular group this is equal to
# @group/mandatory,default,conditional (current
# content of the DNF GROUP_PACKAGE_TYPES constant).
group_spec = "@{}".format(group_name)
include_list.append(group_spec)
# Handle packages.
for pkg_name in data.excluded_packages:
exclude_list.append(pkg_name)
for pkg_name in data.packages:
include_list.append(pkg_name)
return include_list, exclude_list
def get_kernel_version_list():
"""Get a list of installed kernel versions.
:return: a list of kernel versions
"""
files = []
efi_dir = conf.bootloader.efi_dir
# Find all installed RPMs that provide 'kernel'.
ts = rpm.TransactionSet(conf.target.system_root)
mi = ts.dbMatch('providename', 'kernel')
for hdr in mi:
# Find all /boot/vmlinuz- files and strip off vmlinuz-.
files.extend((
f.split("/")[-1][8:] for f in hdr.filenames
if fnmatch.fnmatch(f, "/boot/vmlinuz-*") or
fnmatch.fnmatch(f, "/boot/efi/EFI/%s/vmlinuz-*" % efi_dir)
))
# Sort the kernel versions.
sort_kernel_version_list(files)
return files
def get_free_space_map(current=True, scheduled=False):
"""Get the available file system disk space.
:param bool current: use information about current mount points
:param bool scheduled: use information about scheduled mount points
:return: a dictionary of mount points and their available space
"""
mount_points = {}
if scheduled:
mount_points.update(_get_scheduled_free_space_map())
if current:
mount_points.update(_get_current_free_space_map())
return mount_points
def _get_current_free_space_map():
"""Get the available file system disk space of the current system.
:return: a dictionary of mount points and their available space
"""
mapping = {}
# Generate the dictionary of mount points and sizes.
output = execWithCapture('df', ['--output=target,avail'])
lines = output.rstrip().splitlines()
for line in lines:
key, val = line.rsplit(maxsplit=1)
if not key.startswith('/'):
continue
mapping[key] = Size(int(val) * 1024)
# Add /var/tmp/ if this is a directory or image installation.
if not conf.target.is_hardware:
var_tmp = os.statvfs("/var/tmp")
mapping["/var/tmp"] = Size(var_tmp.f_frsize * var_tmp.f_bfree)
return mapping
def _get_scheduled_free_space_map():
"""Get the available file system disk space of the scheduled system.
:return: a dictionary of mount points and their available space
"""
device_tree = STORAGE.get_proxy(DEVICE_TREE)
mount_points = {}
for mount_point in device_tree.GetMountPoints():
# we can ignore swap
if not mount_point.startswith('/'):
continue
free_space = Size(
device_tree.GetFileSystemFreeSpace([mount_point])
)
mount_point = os.path.normpath(
conf.target.system_root + mount_point
)
mount_points[mount_point] = free_space
return mount_points
def _pick_mount_points(mount_points, download_size, install_size):
"""Pick mount points for the package installation.
:return: a set of sufficient mount points
"""
suitable = {
'/var/tmp',
conf.target.system_root,
join_paths(conf.target.system_root, 'home'),
join_paths(conf.target.system_root, 'tmp'),
join_paths(conf.target.system_root, 'var'),
}
sufficient = set()
for mount_point, size in mount_points.items():
# Ignore mount points that are not suitable.
if mount_point not in suitable:
continue
if size >= (download_size + install_size):
log.debug("Considering %s (%s) for download and install.", mount_point, size)
sufficient.add(mount_point)
elif size >= download_size and not mount_point.startswith(conf.target.system_root):
log.debug("Considering %s (%s) for download.", mount_point, size)
sufficient.add(mount_point)
return sufficient
def _get_biggest_mount_point(mount_points, sufficient):
"""Get the biggest sufficient mount point.
:return: a mount point or None
"""
return max(sufficient, default=None, key=mount_points.get)
def pick_download_location(dnf_manager):
"""Pick the download location.
:param dnf_manager: the DNF manager
:return: a path to the download location
"""
download_size = dnf_manager.get_download_size()
install_size = dnf_manager.get_installation_size()
mount_points = get_free_space_map()
# Try to find mount points that are sufficient for download and install.
sufficient = _pick_mount_points(
mount_points,
download_size,
install_size
)
# Or find mount points that are sufficient only for download.
if not sufficient:
sufficient = _pick_mount_points(
mount_points,
download_size,
install_size=0
)
# Ignore the system root if there are other mount points.
if len(sufficient) > 1:
sufficient.discard(conf.target.system_root)
if not sufficient:
raise RuntimeError(
"Not enough disk space to download the "
"packages; size {}.".format(download_size)
)
# Choose the biggest sufficient mount point.
mount_point = _get_biggest_mount_point(mount_points, sufficient)
log.info("Mount point %s picked as download location", mount_point)
location = join_paths(mount_point, DNF_PACKAGE_CACHE_DIR_SUFFIX)
return location
def calculate_required_space(dnf_manager):
"""Calculate the space required for the installation.
:param DNFManager dnf_manager: the DNF manager
:return Size: the required space
"""
installation_size = dnf_manager.get_installation_size()
download_size = dnf_manager.get_download_size()
mount_points = get_free_space_map(scheduled=True)
# Find sufficient mount points.
sufficient = _pick_mount_points(
mount_points,
download_size,
installation_size
)
# Choose the biggest sufficient mount point.
mount_point = _get_biggest_mount_point(mount_points, sufficient)
if not mount_point or mount_point.startswith(conf.target.system_root):
log.debug("The install and download space is required.")
required_space = installation_size + download_size
else:
log.debug("Use the %s mount point for the %s download.", mount_point, download_size)
log.debug("Only the install space is required.")
required_space = installation_size
log.debug("The package installation requires %s.", required_space)
return required_space
def collect_installation_devices(sources, repositories):
"""Collect devices of installation sources.
:return: a list of device specifications
"""
devices = set()
configurations = [
s.configuration
for s in sources
if s.type == SourceType.HDD
]
for repository in configurations + repositories:
if repository.url.startswith("hd:"):
device, _path = parse_hdd_url(repository.url)
devices.add(device)
return devices
def protect_installation_devices(previous_devices, current_devices):
"""Protect installation devices.
:param previous_devices: a list of device specifications
:param current_devices: a list of device specifications
"""
# Nothing has changed.
if previous_devices == current_devices:
return
disk_selection_proxy = STORAGE.get_proxy(DISK_SELECTION)
protected_devices = disk_selection_proxy.ProtectedDevices
# Remove previous devices from the list.
for spec in previous_devices:
if spec in protected_devices:
protected_devices.remove(spec)
# Add current devices from the list.
for spec in sorted(current_devices):
if spec not in protected_devices:
protected_devices.append(spec)
disk_selection_proxy.ProtectedDevices = protected_devices
def transaction_has_errors(transaction):
"""Detect if finished DNF transaction has any errors.
:param transaction: the DNF transaction
:return: True if the transaction has any error, otherwise False
"""
has_errors = False
for tsi in transaction:
if tsi.state == TransactionItemState_ERROR:
log.error("The transaction contains item %s in error state.", tsi)
has_errors = True
return has_errors