Skip to content

Commit

Permalink
Add hardcoded repofiles for EUS (#458)
Browse files Browse the repository at this point in the history
Allow conversions to RHEL 8.4 EUS minor version

From CentOS and Oracle Linux 8.4.

* Add official CentOS Linux repositories for EUS

Add the official CentOS Linux repositories for versions 8.4 & 8.5 to the data folder to
enable EUS support for those systems.

* Update the functions that check if there is any package to be updated with the
support of the EUS.

This change reflects only the DNF API as the EUS is only supported in RHEL8+
and we manage the CentOS Linux 8+ repos.

* Add a new option to the config files

Add another option to the config files to represent the eus repoids in RHEL.
Since we are supporting only EUS in RHEL8 and not 7, we are just adding these
repoids to the specific versions. The RHEL7 config file option is empty on purpose.

* Remove old rhn* packages from OL8 config

The rhn* packages are causing problems during the conversion in OL8.4 (EUS),
since in the EUS version we are not enabling the appstream repositories (as
OracleLinux does not make appstream public for older versions) we were facing
problems with these specific packages.

Since the rhn* packages were used in the past in conjunction with Satellite 5,
and both are EOL, we decided to remove this package as it does not affect the
conversion in any way and it should not be used.

* Disable new checks for OL8.4

We have decided to disable the checks `is_installed_kernel` and
`check_package_updates` for OL8.4.

The reason for that, is that every OL distribution has enabled by default the
repository `ol8_baseos_latest`, which will have the latest packages available
when a new release is made public (8.6, 8.7, 8.8 and so on...)

Since we can't easily tell whether the system is the latest OL release, we voted to
just disable the checks for any version that is below OL8.latest, that means, for
now, OL8.6.

* Add a rhsm releasever lock for EUS conversions

Without the RHSM releasever lock, any yum call after the conversion would fail as
the baseurl of EUS repositories points to a non-existing URL ($releasever is expanded
to '8' by default instead of the needed '8.4', for instance).

Jira reference (If any): https://issues.redhat.com/browse/OAMG-6511

Signed-off-by: Rodolfo Olivieri <[email protected]>
Co-authored-by: Martin Litwora <[email protected]>
Co-authored-by: Daniel Diblik <[email protected]>
  • Loading branch information
3 people authored Jun 2, 2022
1 parent 8d72fb0 commit 82c661b
Show file tree
Hide file tree
Showing 76 changed files with 2,355 additions and 484 deletions.
2 changes: 1 addition & 1 deletion .packit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ jobs:
epel-7-x86_64:
distros: [centos-7, oraclelinux-7]
epel-8-x86_64:
distros: [centos-8, oraclelinux-8]
distros: [centos-8.4, centos-8, oraclelinux-8.4, oraclelinux-8]
use_internal_tf: True
trigger: pull_request
209 changes: 209 additions & 0 deletions convert2rhel/backup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
#
# Copyright(C) 2022 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <https://www.gnu.org/licenses/>.

import logging
import os
import shutil

from convert2rhel.repo import get_hardcoded_repofiles_dir
from convert2rhel.systeminfo import system_info
from convert2rhel.utils import BACKUP_DIR, download_pkg, remove_orphan_folders, run_subprocess


loggerinst = logging.getLogger(__name__)


class ChangedRPMPackagesController(object):
"""Keep control of installed/removed RPM pkgs for backup/restore."""

def __init__(self):
self.installed_pkgs = []
self.removed_pkgs = []

def track_installed_pkg(self, pkg):
"""Add a installed RPM pkg to the list of installed pkgs."""
self.installed_pkgs.append(pkg)

def track_installed_pkgs(self, pkgs):
"""Track packages installed before the PONR to be able to remove them later (roll them back) if needed."""
self.installed_pkgs += pkgs

def backup_and_track_removed_pkg(self, pkg):
"""Add a removed RPM pkg to the list of removed pkgs."""
restorable_pkg = RestorablePackage(pkg)
restorable_pkg.backup()
self.removed_pkgs.append(restorable_pkg)

def _remove_installed_pkgs(self):
"""For each package installed during conversion remove it."""
loggerinst.task("Rollback: Removing installed packages")
remove_pkgs(self.installed_pkgs, backup=False, critical=False)

def _install_removed_pkgs(self):
"""For each package removed during conversion install it."""
loggerinst.task("Rollback: Installing removed packages")
pkgs_to_install = []
for restorable_pkg in self.removed_pkgs:
if restorable_pkg.path is None:
loggerinst.warning("Couldn't find a backup for %s package." % restorable_pkg.name)
continue
pkgs_to_install.append(restorable_pkg.path)

self._install_local_rpms(pkgs_to_install, replace=True, critical=False)

def _install_local_rpms(self, pkgs_to_install, replace=False, critical=True):
"""Install packages locally available."""

if not pkgs_to_install:
loggerinst.info("No package to install")
return False

cmd_param = ["rpm", "-i"]
if replace:
cmd_param.append("--replacepkgs")

loggerinst.info("Installing packages:")
for pkg in pkgs_to_install:
loggerinst.info("\t%s" % pkg)

cmd = cmd_param + pkgs_to_install
output, ret_code = run_subprocess(cmd, print_output=False)
if ret_code != 0:
pkgs_as_str = " ".join(pkgs_to_install)
loggerinst.debug(output.strip())
if critical:
loggerinst.critical("Error: Couldn't install %s packages." % pkgs_as_str)

loggerinst.warning("Couldn't install %s packages." % pkgs_as_str)
return False

for path in pkgs_to_install:
nvra, _ = os.path.splitext(os.path.basename(path))
self.track_installed_pkg(nvra)

return True

def restore_pkgs(self):
"""Restore system to the original state."""
self._remove_installed_pkgs()
remove_orphan_folders()
self._install_removed_pkgs()


class RestorableFile(object):
def __init__(self, filepath):
self.filepath = filepath

def backup(self):
"""Save current version of a file"""
loggerinst.info("Backing up %s." % self.filepath)
if os.path.isfile(self.filepath):
try:
loggerinst.debug("Copying %s to %s." % (self.filepath, BACKUP_DIR))
shutil.copy2(self.filepath, BACKUP_DIR)
except (OSError, IOError) as err:
# IOError for py2 and OSError for py3
loggerinst.critical("Error(%s): %s" % (err.errno, err.strerror))
else:
loggerinst.info("Can't find %s.", self.filepath)

def restore(self):
"""Restore a previously backed up file"""
backup_filepath = os.path.join(BACKUP_DIR, os.path.basename(self.filepath))
loggerinst.task("Rollback: Restoring %s from backup" % self.filepath)

if not os.path.isfile(backup_filepath):
loggerinst.warning("%s hasn't been backed up" % self.filepath)
return
try:
shutil.copy2(backup_filepath, self.filepath)
except (OSError, IOError) as err:
# Do not call 'critical' which would halt the program. We are in
# a rollback phase now and we want to rollback as much as possible.
# IOError for py2 and OSError for py3
loggerinst.warning("Error(%s): %s" % (err.errno, err.strerror))
return
loggerinst.info("File %s restored" % self.filepath)


class RestorablePackage(object):
def __init__(self, pkgname):
self.name = pkgname
self.path = None

def backup(self):
"""Save version of RPM package"""
loggerinst.info("Backing up %s" % self.name)
if os.path.isdir(BACKUP_DIR):
reposdir = get_hardcoded_repofiles_dir()

# One of the reasons we hardcode repofiles pointing to archived repositories of older system
# minor versions is that we need to be able to download an older package version as a backup.
# Because for example the default repofiles on CentOS Linux 8.4 point only to 8.latest repositories
# that already don't contain 8.4 packages.
if not system_info.has_internet_access:
if reposdir:
loggerinst.debug(
"Not using repository files stored in %s due to the absence of internet access." % reposdir
)
self.path = download_pkg(self.name, dest=BACKUP_DIR, set_releasever=False)
else:
if reposdir:
loggerinst.debug("Using repository files stored in %s." % reposdir)
self.path = download_pkg(
self.name,
dest=BACKUP_DIR,
set_releasever=False,
reposdir=reposdir,
)
else:
loggerinst.warning("Can't access %s" % BACKUP_DIR)


def remove_pkgs(pkgs_to_remove, backup=True, critical=True):
"""Remove packages not heeding to their dependencies."""

# NOTE(r0x0d): This function is tied to the class ChangedRPMPackagesController and
# a couple of other places too, ideally, we should decide if we want to use
# this function as an entrypoint or the variable `changed_pkgs_control`, so
# we can move this piece of code to the `pkghandler.py` where it should be.
# Right now, if we move this code to the `pkghandler.py`, we have a
# *circular import dependency error*.
# @abadger has an implementation in mind to address some of those issues
# and actually place a controller in front of classes like this.
if backup:
# Some packages, when removed, will also remove repo files, making it
# impossible to access the repositories to download a backup. For this
# reason we first backup all packages and only after that we remove
for nvra in pkgs_to_remove:
changed_pkgs_control.backup_and_track_removed_pkg(nvra)

if not pkgs_to_remove:
loggerinst.info("No package to remove")
return

for nvra in pkgs_to_remove:
loggerinst.info("Removing package: %s" % nvra)
_, ret_code = run_subprocess(["rpm", "-e", "--nodeps", nvra])
if ret_code != 0:
if critical:
loggerinst.critical("Error: Couldn't remove %s." % nvra)
else:
loggerinst.warning("Couldn't remove %s." % nvra)


changed_pkgs_control = ChangedRPMPackagesController() # pylint: disable=C0103
82 changes: 67 additions & 15 deletions convert2rhel/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
get_pkg_fingerprint,
get_total_packages_to_update,
)
from convert2rhel.repo import get_hardcoded_repofiles_dir
from convert2rhel.systeminfo import system_info
from convert2rhel.toolopts import tool_opts
from convert2rhel.utils import ask_to_continue, get_file_content, run_subprocess
Expand Down Expand Up @@ -466,9 +467,26 @@ def check_package_updates():
"""Ensure that the system packages installed are up-to-date."""
logger.task("Prepare: Checking if the packages are up-to-date")

if system_info.id == "oracle" and system_info.corresponds_to_rhel_eus_release():
logger.info(
"Skipping the check because there are no publicly available %s %d.%d repositories available."
% (system_info.name, system_info.version.major, system_info.version.minor)
)
return

reposdir = get_hardcoded_repofiles_dir()

if reposdir and not system_info.has_internet_access:
logger.warning("Skipping the check as no internet connection has been detected.")
return

try:
packages_to_update = get_total_packages_to_update()
packages_to_update = get_total_packages_to_update(reposdir=reposdir)
except pkgmanager.RepoError as e:
# As both yum and dnf have the same error class (RepoError), to identify any problems when interacting with the
# repositories, we use this to catch exceptions when verifying if there is any packages to update on the system.
# Beware that the `RepoError` exception is based on the `pkgmanager` module and the message sent to the output
# can differ depending if the code is running in RHEL7 (yum) or RHEL8 (dnf).
logger.warning(
"There was an error while checking whether the installed packages are up-to-date. Having updated system is "
"an important prerequisite for a successful conversion. Consider stopping the conversion to "
Expand All @@ -479,12 +497,15 @@ def check_package_updates():
return

if len(packages_to_update) > 0:
repos_message = (
"on the enabled system repos" if not reposdir else "on repositories defined in the %s folder" % reposdir
)
logger.warning(
"The system has %s packages not updated.\n"
"The system has %s packages not updated based %s.\n"
"List of packages to update: %s.\n\n"
"Not updating the packages may cause the conversion to fail.\n"
"Consider stopping the conversion and update the packages before re-running convert2rhel."
% (len(packages_to_update), " ".join(packages_to_update))
% (len(packages_to_update), repos_message, " ".join(packages_to_update))
)
ask_to_continue()
else:
Expand All @@ -495,15 +516,40 @@ def is_loaded_kernel_latest():
"""Check if the loaded kernel is behind or of the same version as in yum repos."""
logger.task("Prepare: Checking if the loaded kernel version is the most recent")

if system_info.id == "oracle" and system_info.corresponds_to_rhel_eus_release():
logger.info(
"Skipping the check because there are no publicly available %s %d.%d repositories available."
% (system_info.name, system_info.version.major, system_info.version.minor)
)
return

reposdir = get_hardcoded_repofiles_dir()

if reposdir and not system_info.has_internet_access:
logger.warning("Skipping the check as no internet connection has been detected.")
return

cmd = ["repoquery", "--quiet", "--qf", '"%{BUILDTIME}\\t%{VERSION}-%{RELEASE}\\t%{REPOID}"']

# If the reposdir variable is not empty, meaning that it detected the hardcoded repofiles, we should use that
# instead of the system repositories located under /etc/yum.repos.d
if reposdir:
cmd.append("--setopt=reposdir=%s" % reposdir)

# For Oracle/CentOS Linux 8 the `kernel` is just a meta package, instead, we check for `kernel-core`.
# But for 6 and 7 releases, the correct way to check is using `kernel`.
package_to_check = "kernel-core" if system_info.version.major >= 8 else "kernel"

# The latest kernel version on repo
repoquery_output, _ = run_subprocess(
["repoquery", "--quiet", "--qf", '"%{BUILDTIME}\\t%{VERSION}-%{RELEASE}"', package_to_check],
print_output=False,
)
# Append the package name as the last item on the list
cmd.append(package_to_check)

# Search for available kernel package versions available in different repositories using the `repoquery` command.
# If convert2rhel is running on a EUS system, then repoquery will use the hardcoded repofiles available under
# /usr/share/convert2rhel/repos, meaning that the tool will fetch only the latest kernels available for that EUS
# version, and not the most updated version from other newer versions.
# If the case is that convert2rhel is not running on a EUS system, for example, Oracle Linux 8.5, then it will use
# the system repofiles.
repoquery_output, _ = run_subprocess(cmd, print_output=False)

# Repoquery doesn't return any text at all when it can't find any matches for the query (when used with --quiet)
if len(repoquery_output) > 0:
Expand All @@ -513,16 +559,17 @@ def is_loaded_kernel_latest():
# This later check is supposed to avoid duplicate repofiles being defined in the system,
# this is a super corner case and should not happen everytime, but if it does, we are aware now.
packages = [
tuple(str(line).split("\t"))
tuple(str(line).replace('"', "").split("\t"))
for line in repoquery_output.split("\n")
if (line.strip() and "listed more than once in the configuration" not in line.lower())
]

# Sort out for the most recent kernel with reverse order
# In case `repoquery` returns more than one kernel in the output
# We display the latest one to the user.
_, latest_kernel = sorted(packages, reverse=True)[0]
latest_kernel = latest_kernel.replace('"', "")
packages.sort(key=lambda x: x[0], reverse=True)

_, latest_kernel, repoid = packages[0]

# The loaded kernel version
uname_output, _ = run_subprocess(["uname", "-r"], print_output=False)
Expand All @@ -532,13 +579,18 @@ def is_loaded_kernel_latest():
if match == 0:
logger.info("Kernel currently loaded is at the latest version.")
else:
repos_message = (
"on the enabled system repositories"
if not reposdir
else "on repositories defined in the %s folder" % reposdir
)
logger.critical(
"The current kernel version loaded is different from the latest version in your repos.\n"
" Latest kernel version: %s\n"
" Current loaded kernel: %s\n\n"
"The version of the loaded kernel is different from the latest version %s.\n"
" Latest kernel version available in %s: %s\n"
" Loaded kernel version: %s\n\n"
"To proceed with the conversion, update the kernel version by executing the following step:\n\n"
"1. yum install %s-%s -y\n"
"2. reboot" % (latest_kernel, loaded_kernel, package_to_check, latest_kernel)
"2. reboot" % (repos_message, repoid, latest_kernel, loaded_kernel, package_to_check, latest_kernel)
)
else:
# Repoquery failed to detected any kernel or kernel-core packages in it's repositories
Expand Down
4 changes: 4 additions & 0 deletions convert2rhel/data/7/x86_64/configs/centos-7-x86_64.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ repofile_pkgs =
# Delimited by any whitespace(s).
default_rhsm_repoids = rhel-7-server-rpms

# List of Extended Update Support (EUS) repoids to enable through subscription-manager when the --enablerepo option is
# not used. Delimited by any whitespace(s).
eus_rhsm_repoids =

# If defined, it overrides the default releasever defined by RELEASE_VER_MAPPING.
# The value is passed to the yum calls through the --releasever option when accessing RHEL repos. Its purpose is to
# substitute the $releasever variable in a repository baseurl.
Expand Down
4 changes: 4 additions & 0 deletions convert2rhel/data/7/x86_64/configs/oracle-7-x86_64.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ repofile_pkgs =
# Delimited by any whitespace(s).
default_rhsm_repoids = rhel-7-server-rpms

# List of Extended Update Support (EUS) repoids to enable through subscription-manager when the --enablerepo option is
# not used. Delimited by any whitespace(s).
eus_rhsm_repoids =

# If defined, it overrides the default releasever defined by RELEASE_VER_MAPPING.
# The value is passed to the yum calls through the --releasever option when accessing RHEL repos. Its purpose is to
# substitute the $releasever variable in a repository baseurl.
Expand Down
4 changes: 4 additions & 0 deletions convert2rhel/data/7/x86_64/configs/scientific-7-x86_64.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ repofile_pkgs =
# Delimited by any whitespace(s).
default_rhsm_repoids = rhel-7-server-rpms rhel-7-server-optional-rpms

# List of Extended Update Support (EUS) repoids to enable through subscription-manager when the --enablerepo option is
# not used. Delimited by any whitespace(s).
eus_rhsm_repoids =

# If defined, it overrides the default releasever defined by RELEASE_VER_MAPPING.
# The value is passed to the yum calls through the --releasever option when accessing RHEL repos. Its purpose is to
# substitute the $releasever variable in a repository baseurl.
Expand Down
Loading

0 comments on commit 82c661b

Please sign in to comment.