Skip to content

Commit 2d06979

Browse files
committed
Handle yum not accepting --setopt=varsdir=
The --setopt=varsdir= option was introduced in dnf. For that reason we can't pass to yum a directory with yum variable files and instead we need to ensure that all necessary yum vars are defined in /etc/yum/vars.
1 parent 1cbc5d4 commit 2d06979

File tree

15 files changed

+216
-71
lines changed

15 files changed

+216
-71
lines changed

convert2rhel/actions/pre_ponr_changes/backup_system.py

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
from convert2rhel.logger import LOG_DIR, root_logger
2525
from convert2rhel.pkghandler import VERSIONLOCK_FILE_PATH
2626
from convert2rhel.redhatrelease import os_release_file, system_release_file
27-
from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR
28-
from convert2rhel.systeminfo import system_info
27+
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR
2928
from convert2rhel.toolopts import tool_opts
3029
from convert2rhel.utils import warn_deprecated_env
3130
from convert2rhel.utils.rpm import PRE_RPM_VA_LOG_FILENAME
@@ -100,40 +99,6 @@ def run(self):
10099
backup.backup_control.push(restorable_file)
101100

102101

103-
class BackupYumVariables(actions.Action):
104-
id = "BACKUP_YUM_VARIABLES"
105-
106-
def run(self):
107-
"""Backup varsdir folder in /etc/{yum,dnf}/vars so the variables can be restored on rollback."""
108-
logger.task("Backup variables")
109-
110-
super(BackupYumVariables, self).run()
111-
112-
logger.info("Backing up variables files from {}.".format(DEFAULT_YUM_VARS_DIR))
113-
self._backup_variables(path=DEFAULT_YUM_VARS_DIR)
114-
115-
if system_info.version.major >= 8:
116-
logger.info("Backing up variables files from {}.".format(DEFAULT_DNF_VARS_DIR))
117-
self._backup_variables(path=DEFAULT_DNF_VARS_DIR)
118-
119-
def _backup_variables(self, path):
120-
"""Helper internal function to backup the variables.
121-
122-
:param path: The path for the original variable.
123-
:type path: str
124-
"""
125-
variable_files_backed_up = False
126-
127-
for variable in os.listdir(path):
128-
variable_path = os.path.join(path, variable)
129-
restorable_file = RestorableFile(variable_path)
130-
backup.backup_control.push(restorable_file)
131-
variable_files_backed_up = True
132-
133-
if not variable_files_backed_up:
134-
logger.info("No variables files backed up.")
135-
136-
137102
class BackupPackageFiles(actions.Action):
138103
id = "BACKUP_PACKAGE_FILES"
139104
# BACKUP_PACKAGE_FILES should be the last one

convert2rhel/actions/pre_ponr_changes/handle_packages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class RemoveSpecialPackages(actions.Action):
7777
"BACKUP_REPOSITORY",
7878
"BACKUP_PACKAGE_FILES",
7979
"BACKUP_REDHAT_RELEASE",
80+
"BACKUP_YUM_VARIABLES",
8081
)
8182

8283
def run(self):
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright(C) 2025 Red Hat, Inc.
2+
#
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
__metaclass__ = type
17+
18+
import os
19+
import shutil
20+
21+
from convert2rhel import actions
22+
from convert2rhel import backup
23+
from convert2rhel.backup.files import InstalledFile, RestorableFile
24+
from convert2rhel.logger import root_logger
25+
from convert2rhel.pkghandler import get_files_owned_by_package, get_packages_to_remove
26+
from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR
27+
from convert2rhel.systeminfo import system_info
28+
from convert2rhel.toolopts.config import loggerinst
29+
30+
logger = root_logger.getChild(__name__)
31+
32+
33+
class BackUpYumVariables(actions.Action):
34+
id = "BACKUP_YUM_VARIABLES"
35+
# We don't make a distinction between /etc/yum/vars/ and /etc/yum/vars/ in this Action. Wherever the files are we
36+
# back them up.
37+
yum_var_dirs = [DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR]
38+
39+
def run(self):
40+
"""Back up yum variable files in /etc/{yum,dnf}/vars/ owned by packages that are known to install these yum
41+
variable files (such as system-release). We back them up to be able to restore them right after we remove these
42+
packages. We need to restore the variable files because we use repofiles also installed by these packages and
43+
yum does not allow specifying a custom directory with yum variable files. This functionality came later with dnf
44+
however we apply the same approach to both yum and dnf for the sake of code simplicity.
45+
"""
46+
logger.task("Back up yum variables")
47+
48+
super(BackUpYumVariables, self).run()
49+
50+
logger.debug("Getting a list of files owned by packages affecting variables in .repo files.")
51+
yum_var_affecting_pkgs = get_packages_to_remove(system_info.repofile_pkgs)
52+
yum_var_filepaths = self._get_yum_var_files_owned_by_pkgs(
53+
[pkg_obj.nevra.name for pkg_obj in yum_var_affecting_pkgs]
54+
)
55+
56+
self._back_up_var_files(yum_var_filepaths)
57+
58+
def _get_yum_var_files_owned_by_pkgs(self, pkg_names):
59+
"""Get paths of yum var files owned by the packages passed to the method."""
60+
pkg_owned_files = set()
61+
for pkg in pkg_names:
62+
pkg_owned_files.union(get_files_owned_by_package(pkg)) # using set() and union() to get unique paths
63+
64+
# Out of all the files owned by the packages get just those in yum/dnf var dirs
65+
yum_var_filepaths = [path for path in pkg_owned_files if os.path.dirname(path) in self.yum_var_dirs]
66+
67+
return yum_var_filepaths
68+
69+
def _back_up_var_files(self, paths):
70+
"""Back up yum variable files.
71+
72+
:param paths: Paths to the variable files to back up
73+
:type paths: list[str]
74+
"""
75+
logger.info("Backing up variables files from {}.".format(" and ".join(self.yum_var_dirs)))
76+
if not paths:
77+
logger.info("No variables files backed up.")
78+
79+
for filepath in paths:
80+
restorable_file = RestorableFile(filepath)
81+
backup.backup_control.push(restorable_file)
82+
83+
84+
class RestoreYumVarFiles(actions.Action):
85+
id = "RESTORE_YUM_VAR_FILES"
86+
dependencies = ("REMOVE_SPECIAL_PACKAGES",)
87+
88+
def run(self):
89+
"""Right after removing packages that own yum variable files in the REMOVE_SPECIAL_PACKAGES Action, in this
90+
Action we restore these files to /etc/{yum,dnf}/vars/ so that yum can use them when accessing the original
91+
vendor repositories (which are backed up in a temporary folder and passed to yum through the --setopt=reposdir=
92+
option).
93+
The ideal solution would be to use the --setopt=varsdir= option also for the temporary folder where yum variable
94+
files are backed up however the option was only introduced in dnf so it's not available in RHEL 7 and its
95+
derivatives. For the sake of using just one approach to simplify the codebase, we are restoring the yum variable
96+
files no matter the package manager.
97+
We use the backup controller to record that we've restored the variable files meaning that upon rollback the
98+
files get removed. As part of the rollback we also install beck the packages that include these files so they'll
99+
be present.
100+
TODO: These restored variable files should not be present after a successful conversion. One option is to
101+
enhance the backup controller to indicate that a certain activity should be rolled back not only during a rollback
102+
but also after a successful conversion. With such a flag we would add a new post-conversion Action to run the
103+
backup controller restoration but only for the activities recorded with this flag.
104+
"""
105+
super(RestoreYumVarFiles, self).run()
106+
107+
backed_up_yum_var_dirs = backup.get_backed_up_yum_var_dirs()
108+
loggerinst.task("Restoring yum variable files")
109+
loggerinst.info(
110+
"In a previous step we removed a package that might have come with yum variables and in case we"
111+
" need to access {} repositories (e.g. when installing dependencies of subscription-manager) we"
112+
" need these yum variables available.".format(system_info.name)
113+
)
114+
for orig_yum_var_dir in backed_up_yum_var_dirs:
115+
for backed_up_yum_var_filepath in os.listdir(backed_up_yum_var_dirs[orig_yum_var_dir]):
116+
try:
117+
shutil.copy2(backed_up_yum_var_filepath, orig_yum_var_dir)
118+
logger.debug("Copied {} from backup to {}.".format(backed_up_yum_var_filepath, orig_yum_var_dir))
119+
except (OSError, IOError) as err:
120+
# IOError for py2 and OSError for py3
121+
# Not being able to restore the yum variables might or might not cause problems down the road. No
122+
# need to stop the conversion because of that. The warning message below should be enough of a clue
123+
# for resolving subsequent yum errors.
124+
logger.warning(
125+
"Couldn't copy {} to {}. Error: {}".format(
126+
backed_up_yum_var_filepath, orig_yum_var_dir, err.strerror
127+
)
128+
)
129+
return
130+
restored_file = InstalledFile(
131+
os.path.join(orig_yum_var_dir, os.path.basename(backed_up_yum_var_filepath))
132+
)
133+
backup.backup_control.push(restored_file)

convert2rhel/backup/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import six
2525

2626
from convert2rhel.logger import root_logger
27-
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR
27+
from convert2rhel.repo import DEFAULT_YUM_REPOFILE_DIR, DEFAULT_YUM_VARS_DIR, DEFAULT_DNF_VARS_DIR
2828
from convert2rhel.utils import TMP_DIR
2929

3030

@@ -43,6 +43,19 @@ def get_backedup_system_repos():
4343
return backedup_reposdir
4444

4545

46+
def get_backed_up_yum_var_dirs():
47+
"""Get folders where we've backed up yum and dnf variables inside our backup structure.
48+
49+
:returns dict: Keys are the original dir paths, values are paths to the dir with backed up yum/dnf variable files
50+
"""
51+
52+
yum_var_dirs = {
53+
DEFAULT_YUM_VARS_DIR: hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest(),
54+
DEFAULT_DNF_VARS_DIR: hashlib.md5(DEFAULT_DNF_VARS_DIR.encode()).hexdigest(),
55+
}
56+
return yum_var_dirs
57+
58+
4659
class BackupController:
4760
"""
4861
Controls backup and restore for all restorable types.

convert2rhel/backup/certs.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ def enable(self):
118118
try:
119119
files.mkdir_p(self._target_cert_dir)
120120
shutil.copy2(self._source_cert_path, self._target_cert_dir)
121-
except OSError as err:
122-
logger.critical_no_exit("OSError({0}): {1}".format(err.errno, err.strerror))
121+
except (OSError, IOError) as err:
122+
# IOError for py2 and OSError for py3
123+
logger.critical_no_exit("Error({0}): {1}".format(err.errno, err.strerror))
123124
raise exceptions.CriticalError(
124125
id_="FAILED_TO_INSTALL_CERTIFICATE",
125126
title="Failed to install certificate.",

convert2rhel/backup/files.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,44 @@ def restore(self):
208208
logger.info("File {filepath} removed".format(filepath=self.filepath))
209209

210210
super(MissingFile, self).restore()
211+
212+
213+
class InstalledFile(RestorableChange):
214+
"""
215+
A file we plant on the system during the conversion. It can either be removed on a rollback or after a successful
216+
conversion, depending on what purpose the planted file serves.
217+
"""
218+
219+
def __init__(self, filepath):
220+
super(InstalledFile, self).__init__()
221+
self.filepath = filepath
222+
223+
def enable(self):
224+
if self.enabled:
225+
return
226+
227+
logger.info("Marking file {filepath} as installed on the system.".format(filepath=self.filepath))
228+
super(InstalledFile, self).enable()
229+
230+
def restore(self):
231+
"""Remove the file if it was installed during the conversion.
232+
233+
.. warning::
234+
Exceptions are not handled and left for handling by the calling code.
235+
236+
:raises OSError: When the removal of the file fails.
237+
:raises IOError: When the removal of the file fails.
238+
"""
239+
if not self.enabled:
240+
return
241+
242+
logger.task("Remove {filepath} installed during the conversion".format(filepath=self.filepath))
243+
244+
if not os.path.isfile(self.filepath):
245+
logger.info("File {filepath} wasn't installed during conversion.".format(filepath=self.filepath))
246+
else:
247+
# Possible exceptions will be handled in the BackupController
248+
os.remove(self.filepath)
249+
logger.info("File {filepath} removed.".format(filepath=self.filepath))
250+
251+
super(InstalledFile, self).restore()

convert2rhel/data/7/x86_64/configs/amazon-2-x86_64.cfg

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[system_info]
22

3-
# The GPG Key IDs used for signing packages of the source OS. They are available at https://cdn.amazonlinux.com/_assets/11CF1F95C87F5B1A.asc
3+
# The GPG Key ID used for signing packages of the source OS. It is available at https://cdn.amazonlinux.com/_assets/11CF1F95C87F5B1A.asc.
44
# Delimited by whitespace(s).
55
gpg_key_ids = 11cf1f95c87f5b1a
66

@@ -53,4 +53,3 @@ releasever=
5353
# Some kernel modules move from kernel modules to kernel core. Instead of inhibiting the conversion with a message
5454
# that such a module is not available in RHEL and thus is unsupported, we ignore it.
5555
kmods_to_ignore =
56-
kernel/drivers/input/ff-memless.ko.xz

convert2rhel/pkghandler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ def get_installed_pkgs_w_different_key_id(key_ids, name="*"):
292292
return [pkg for pkg in pkgs_w_key_ids if pkg.key_id not in key_ids and pkg.nevra.name != "gpg-pubkey"]
293293

294294

295+
@utils.run_as_child_process
296+
def get_files_owned_by_package(installed_pkg_name):
297+
"""Get a list of files that are owned by an installed package."""
298+
output, ret_code = utils.run_subprocess(["/usr/bin/rpm", "-ql", installed_pkg_name])
299+
if ret_code != 0:
300+
logger.warning("Failed to list files for package {0}: {1}".format(installed_pkg_name, output))
301+
return []
302+
return output.decode("utf-8").splitlines()
303+
304+
295305
@utils.run_as_child_process
296306
def format_pkg_info(pkgs, disable_repos=None):
297307
"""Format package information.

convert2rhel/pkgmanager/handlers/yum/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
__metaclass__ = type
1919

20-
import hashlib
21-
import os
2220
import re
2321

2422
from convert2rhel import backup, exceptions, pkgmanager, utils
@@ -27,7 +25,6 @@
2725
from convert2rhel.pkghandler import get_system_packages_for_replacement
2826
from convert2rhel.pkgmanager.handlers.base import TransactionHandlerBase
2927
from convert2rhel.pkgmanager.handlers.yum.callback import PackageDownloadCallback, TransactionDisplayCallback
30-
from convert2rhel.repo import DEFAULT_YUM_VARS_DIR
3128
from convert2rhel.systeminfo import system_info
3229
from convert2rhel.utils import remove_pkgs
3330

@@ -74,15 +71,13 @@ def _resolve_yum_problematic_dependencies(output):
7471
"\n".join(packages_to_remove),
7572
)
7673
backedup_reposdir = backup.get_backedup_system_repos()
77-
backedup_yum_varsdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(DEFAULT_YUM_VARS_DIR.encode()).hexdigest())
7874

7975
backup.backup_control.push(
8076
RestorablePackage(
8177
pkgs=packages_to_remove,
8278
reposdir=backedup_reposdir,
8379
set_releasever=True,
8480
custom_releasever=system_info.version.major,
85-
varsdir=backedup_yum_varsdir,
8681
)
8782
)
8883
remove_pkgs(pkgs_to_remove=packages_to_remove, critical=True)

convert2rhel/subscription.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
__metaclass__ = type
1919

20-
import hashlib
2120
import json
2221
import os
2322
import re
@@ -33,7 +32,6 @@
3332
from convert2rhel.backup.packages import RestorablePackageSet
3433
from convert2rhel.logger import root_logger
3534
from convert2rhel.redhatrelease import os_release_file
36-
from convert2rhel.repo import DEFAULT_DNF_VARS_DIR, DEFAULT_YUM_VARS_DIR
3735
from convert2rhel.systeminfo import system_info
3836
from convert2rhel.toolopts import tool_opts
3937
from convert2rhel.utils.subscription import _should_subscribe
@@ -610,8 +608,6 @@ def install_rhel_subscription_manager(pkgs_to_install):
610608
shortcoming by installing the certificate.
611609
"""
612610
backedup_reposdir = backup.get_backedup_system_repos()
613-
varsdir = DEFAULT_YUM_VARS_DIR if system_info.version.major == 7 else DEFAULT_DNF_VARS_DIR
614-
backedup_varsdir = os.path.join(backup.BACKUP_DIR, hashlib.md5(varsdir.encode()).hexdigest())
615611

616612
setopts = []
617613
# Oracle Linux 7 needs to set the exclude option to avoid installing the
@@ -632,7 +628,6 @@ def install_rhel_subscription_manager(pkgs_to_install):
632628

633629
reposdir = [os.path.dirname(client_tools_repofile_path), backedup_reposdir]
634630
setopts.append("reposdir={}".format(",".join(reposdir)))
635-
setopts.append("varsdir={}".format(backedup_varsdir))
636631
installed_pkg_set = RestorablePackageSet(
637632
pkgs_to_install,
638633
custom_releasever=system_info.version.major,

0 commit comments

Comments
 (0)