Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept for Amazon Linux 2 conversions #1455

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion convert2rhel/actions/post_conversion/update_grub.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,44 @@
from convert2rhel import actions, backup, grub, utils
from convert2rhel.backup.files import RestorableFile
from convert2rhel.logger import root_logger

from convert2rhel.systeminfo import system_info

logger = root_logger.getChild(__name__)


class FixGrubSettingsOnAL2(actions.Action):
id = "FIX_GRUB_SETTINGS_ON_AL2"

def run(self):
"""On Amazon Linux 2 the GRUB_TERMINAL setting in /etc/default/grub is set to "ec2-console". Leaving it there
prevents us from successfully executing grub2-mkconfig - we need to change that to "console".
"""
super(FixGrubSettingsOnAL2, self).run()

logger.task("Fix GRUB2 settings on Amazon Linux 2")
if system_info.version.major != 2:
logger.info("Not running Amazon Linux 2, skipping.")
return

file_path = "/etc/default/grub"
old_value = 'GRUB_TERMINAL="ec2-console"'
new_value = 'GRUB_TERMINAL="console"'

content = utils.get_file_content(file_path)
if old_value in content:
updated_content = content.replace(old_value, new_value)

with open("/etc/default/grub", "w") as file:
file.write(updated_content)
logger.debug("Replaced {} with {} in {}.".format(old_value, new_value, file_path))
logger.info("Successfully updated /etc/default/grub.")
else:
logger.info("{} not found in {}. Nothing to do.".format(old_value, file_path))


class UpdateGrub(actions.Action):
id = "UPDATE_GRUB"
dependencies = ("FIX_GRUB_SETTINGS_ON_AL2",)

def run(self):
"""Update GRUB2 images and config after conversion.
Expand Down
39 changes: 2 additions & 37 deletions convert2rhel/actions/pre_ponr_changes/backup_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
from convert2rhel.logger import LOG_DIR, root_logger
from convert2rhel.pkghandler import VERSIONLOCK_FILE_PATH
from convert2rhel.redhatrelease import os_release_file, system_release_file
from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR
from convert2rhel.systeminfo import system_info
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR
from convert2rhel.toolopts import tool_opts
from convert2rhel.utils import warn_deprecated_env
from convert2rhel.utils.rpm import PRE_RPM_VA_LOG_FILENAME
Expand Down Expand Up @@ -100,47 +99,13 @@ def run(self):
backup.backup_control.push(restorable_file)


class BackupYumVariables(actions.Action):
id = "BACKUP_YUM_VARIABLES"

def run(self):
"""Backup varsdir folder in /etc/{yum,dnf}/vars so the variables can be restored on rollback."""
logger.task("Backup variables")

super(BackupYumVariables, self).run()

logger.info("Backing up variables files from {}.".format(DEFAULT_YUM_VARS_DIR))
self._backup_variables(path=DEFAULT_YUM_VARS_DIR)

if system_info.version.major >= 8:
logger.info("Backing up variables files from {}.".format(DEFAULT_DNF_VARS_DIR))
self._backup_variables(path=DEFAULT_DNF_VARS_DIR)

def _backup_variables(self, path):
"""Helper internal function to backup the variables.

:param path: The path for the original variable.
:type path: str
"""
variable_files_backed_up = False

for variable in os.listdir(path):
variable_path = os.path.join(path, variable)
restorable_file = RestorableFile(variable_path)
backup.backup_control.push(restorable_file)
variable_files_backed_up = True

if not variable_files_backed_up:
logger.info("No variables files backed up.")


class BackupPackageFiles(actions.Action):
id = "BACKUP_PACKAGE_FILES"
# BACKUP_PACKAGE_FILES should be the last one
# Something could be backed up by this function
# and if the MD5 differs it might be backed up for second time
# by the BackupPackageFiles
dependencies = ("BACKUP_REPOSITORY", "BACKUP_REDHAT_RELEASE")
dependencies = ("BACKUP_REPOSITORY", "BACKUP_REDHAT_RELEASE", "BACKUP_YUM_VARIABLES")

def run(self):
"""Backup changed package files"""
Expand Down
1 change: 1 addition & 0 deletions convert2rhel/actions/pre_ponr_changes/handle_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class RemoveSpecialPackages(actions.Action):
"BACKUP_REPOSITORY",
"BACKUP_PACKAGE_FILES",
"BACKUP_REDHAT_RELEASE",
"BACKUP_YUM_VARIABLES",
)

def run(self):
Expand Down
145 changes: 145 additions & 0 deletions convert2rhel/actions/pre_ponr_changes/yum_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright(C) 2025 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/>.

__metaclass__ = type

import os
import shutil

from convert2rhel import actions
from convert2rhel import backup
from convert2rhel.backup.files import InstalledFile, RestorableFile
from convert2rhel.logger import root_logger
from convert2rhel.pkghandler import get_files_owned_by_package, get_packages_to_remove
from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR
from convert2rhel.systeminfo import system_info
from convert2rhel.toolopts.config import loggerinst

logger = root_logger.getChild(__name__)


class BackUpYumVariables(actions.Action):
id = "BACKUP_YUM_VARIABLES"
# We don't make a distinction between /etc/yum/vars/ and /etc/yum/vars/ in this Action. Wherever the files are we
# back them up.
yum_var_dirs = [DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR]

def run(self):
"""Back up yum variable files in /etc/{yum,dnf}/vars/ owned by packages that are known to install these yum
variable files (such as system-release). We back them up to be able to restore them right after we remove these
packages. We need to restore the variable files because we use repofiles also installed by these packages and
yum does not allow specifying a custom directory with yum variable files. This functionality came later with dnf
however we apply the same approach to both yum and dnf for the sake of code simplicity.
"""
logger.task("Back up yum variables")

super(BackUpYumVariables, self).run()

logger.debug("Getting a list of files owned by packages affecting variables in .repo files.")
yum_var_affecting_pkgs = get_packages_to_remove(system_info.repofile_pkgs)
yum_var_files_to_back_up = self._get_yum_var_files_owned_by_pkgs(
[pkg_obj.nevra.name for pkg_obj in yum_var_affecting_pkgs]
)
self._back_up_var_files(yum_var_files_to_back_up)

def _get_yum_var_files_owned_by_pkgs(self, pkg_names):
"""Get paths of yum var files owned by the packages passed to the method."""
pkg_owned_files = set()
for pkg in pkg_names:
# using set() and union() to get unique paths
pkg_owned_files = pkg_owned_files.union(get_files_owned_by_package(pkg))

# Out of all the files owned by the packages get just those in yum/dnf var dirs
yum_var_filepaths = [
path for path in pkg_owned_files if os.path.normcase(os.path.dirname(path)) in self.yum_var_dirs
]

return yum_var_filepaths

def _back_up_var_files(self, paths):
"""Back up yum variable files.

:param paths: Paths to the variable files to back up
:type paths: list[str]
"""
logger.info(
"Backing up variables files from {} owned by {} packages.".format(
" and ".join(self.yum_var_dirs), system_info.name
)
)
if not paths:
logger.info("No variables files backed up.")

for filepath in paths:
restorable_file = RestorableFile(filepath)
backup.backup_control.push(restorable_file)
logger.info("Yum variables successfully backed up.")


class RestoreYumVarFiles(actions.Action):
id = "RESTORE_YUM_VAR_FILES"
dependencies = ("REMOVE_SPECIAL_PACKAGES",)

def run(self):
"""Right after removing packages that own yum variable files in the REMOVE_SPECIAL_PACKAGES Action, in this
Action we restore these files to /etc/{yum,dnf}/vars/ so that yum can use them when accessing the original
vendor repositories (which are backed up in a temporary folder and passed to yum through the --setopt=reposdir=
option).
The ideal solution would be to use the --setopt=varsdir= option also for the temporary folder where yum variable
files are backed up however the option was only introduced in dnf so it's not available in RHEL 7 and its
derivatives. For the sake of using just one approach to simplify the codebase, we are restoring the yum variable
files no matter the package manager.
We use the backup controller to record that we've restored the variable files meaning that upon rollback the
files get removed. As part of the rollback we also install beck the packages that include these files so they'll
be present.
TODO: These restored variable files should not be present after a successful conversion. One option is to
enhance the backup controller to indicate that a certain activity should be rolled back not only during a rollback
but also after a successful conversion. With such a flag we would add a new post-conversion Action to run the
backup controller restoration but only for the activities recorded with this flag.
"""
super(RestoreYumVarFiles, self).run()

backed_up_yum_var_dirs = backup.get_backed_up_yum_var_dirs()
loggerinst.task("Restoring yum variable files")
loggerinst.info(
"We need to restore {0} yum variables as they are oftentimes necessary for accessing the {0} repositories.".format(
system_info.name
)
)
for orig_yum_var_dir in backed_up_yum_var_dirs:
backed_up_yum_var_dir = backed_up_yum_var_dirs[orig_yum_var_dir]
if not os.path.exists(backed_up_yum_var_dir):
logger.info("No file from {} backed up. Nothing to restore.".format(orig_yum_var_dir))
continue
for backed_up_yum_var_filename in os.listdir(backed_up_yum_var_dir):
backed_up_yum_var_filepath = os.path.join(backed_up_yum_var_dir, backed_up_yum_var_filename)
try:
shutil.copy2(backed_up_yum_var_filepath, orig_yum_var_dir)
logger.debug("Copied {} from backup to {}.".format(backed_up_yum_var_filepath, orig_yum_var_dir))
except (OSError, IOError) as err:
# IOError for py2 and OSError for py3
# Not being able to restore the yum variables might or might not cause problems down the road. No
# need to stop the conversion because of that. The warning message below should be enough of a clue
# for resolving subsequent yum errors.
logger.warning(
"Couldn't copy {} to {}. Error: {}".format(
backed_up_yum_var_filepath, orig_yum_var_dir, err.strerror
)
)
return
restored_file = InstalledFile(
os.path.join(orig_yum_var_dir, os.path.basename(backed_up_yum_var_filepath))
)
backup.backup_control.push(restored_file)
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
logger = root_logger.getChild(__name__)

C2R_REPOFILE_URLS = {
2: "https://cdn-public.redhat.com/content/public/addon/dist/convert2rhel/server/7/7Server/x86_64/files/repofile.repo", # Amazon Linux 2
7: "https://cdn-public.redhat.com/content/public/addon/dist/convert2rhel/server/7/7Server/x86_64/files/repofile.repo",
8: "https://cdn-public.redhat.com/content/public/addon/dist/convert2rhel8/8/x86_64/files/repofile.repo",
9: "https://cdn-public.redhat.com/content/public/repofiles/convert2rhel-for-rhel-9-x86_64.repo",
Expand Down
19 changes: 17 additions & 2 deletions convert2rhel/actions/system_checks/rhel_compatible_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

# The kernel version stays the same throughout a RHEL major version
COMPATIBLE_KERNELS_VERS = {
2: "4.18.0", # In Amazon Linux 2 there's no kernel of the same version as in RHEL (there's 4.14 and 5.10)
7: "3.10.0",
8: "4.18.0",
9: "5.14.0",
Expand Down Expand Up @@ -66,6 +67,21 @@ def run(self):
bad_kernel_message = str(e)
logger.warning(bad_kernel_message)

if system_info.version.major == 2:
logger.warning(
"Ignoring the check result on Amazon Linux 2 as there's no kernel of the same"
" version as in RHEL available."
)
self.add_message(
level="WARNING",
id="INCOMPATIBLE_KERNEL_ON_AL2",
title="Incompatible booted kernel version",
description="On Amazon Linux 2 there's no kernel of the same version as in RHEL available."
" The kernel downgrade during the conversion may cause issues, proceed at your"
" own risk.",
diagnosis=bad_kernel_message,
)
return
self.set_result(
level="ERROR",
id=e.error_id,
Expand Down Expand Up @@ -107,11 +123,10 @@ def _bad_kernel_version(kernel_release):
raise KernelIncompatibleError(
"INCOMPATIBLE_VERSION",
"Booted kernel version '{kernel_version}' does not correspond to the version "
"'{compatible_version}' available in RHEL {rhel_major_version}",
"'{compatible_version}' available in RHEL",
{
"kernel_version": kernel_version,
"compatible_version": COMPATIBLE_KERNELS_VERS[system_info.version.major],
"rhel_major_version": system_info.version.major,
},
)

Expand Down
15 changes: 14 additions & 1 deletion convert2rhel/backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import six

from convert2rhel.logger import root_logger
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR, DEFAULT_DNF_VARS_DIR
from convert2rhel.utils import TMP_DIR


Expand All @@ -43,6 +43,19 @@ def get_backedup_system_repos():
return backedup_reposdir


def get_backed_up_yum_var_dirs():
"""Get folders where we've backed up yum and dnf variables inside our backup structure.

:returns dict: Keys are the original dir paths, values are paths to the dir with backed up yum/dnf variable files
"""

yum_var_dirs = {
DEFAULT_YUM_VARS_DIR: os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest()),
DEFAULT_DNF_VARS_DIR: os.path.join(BACKUP_DIR, hashlib.md5(DEFAULT_DNF_VARS_DIR.encode()).hexdigest()),
}
return yum_var_dirs


class BackupController:
"""
Controls backup and restore for all restorable types.
Expand Down
5 changes: 3 additions & 2 deletions convert2rhel/backup/certs.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ def enable(self):
try:
files.mkdir_p(self._target_cert_dir)
shutil.copy2(self._source_cert_path, self._target_cert_dir)
except OSError as err:
logger.critical_no_exit("OSError({0}): {1}".format(err.errno, err.strerror))
except (OSError, IOError) as err:
# IOError for py2 and OSError for py3
logger.critical_no_exit("Error({0}): {1}".format(err.errno, err.strerror))
raise exceptions.CriticalError(
id_="FAILED_TO_INSTALL_CERTIFICATE",
title="Failed to install certificate.",
Expand Down
41 changes: 41 additions & 0 deletions convert2rhel/backup/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,44 @@ def restore(self):
logger.info("File {filepath} removed".format(filepath=self.filepath))

super(MissingFile, self).restore()


class InstalledFile(RestorableChange):
"""
A file we plant on the system during the conversion. It can either be removed on a rollback or after a successful
conversion, depending on what purpose the planted file serves.
"""

def __init__(self, filepath):
super(InstalledFile, self).__init__()
self.filepath = filepath

def enable(self):
if self.enabled:
return

logger.info("Marking file {filepath} as installed on the system.".format(filepath=self.filepath))
super(InstalledFile, self).enable()

def restore(self):
"""Remove the file if it was installed during the conversion.

.. warning::
Exceptions are not handled and left for handling by the calling code.

:raises OSError: When the removal of the file fails.
:raises IOError: When the removal of the file fails.
"""
if not self.enabled:
return

logger.task("Remove {filepath} installed during the conversion".format(filepath=self.filepath))

if not os.path.isfile(self.filepath):
logger.info("File {filepath} wasn't installed during conversion.".format(filepath=self.filepath))
else:
# Possible exceptions will be handled in the BackupController
os.remove(self.filepath)
logger.info("File {filepath} removed.".format(filepath=self.filepath))

super(InstalledFile, self).restore()
Loading
Loading